От сырых COM
API к проекту ATL
В этом уроке мы научимся разрабатывать приложения, которые реализуют функции СОМ-сервера и СОМ-контейнера. Известная вам технология OLE (Object Linking and Embedding) базируется на модели COM (Component Object Model), которая определяет и реализует механизм, позволяющий отдельным компонентам (приложениям, объектам данных, элементам управления, сервисам) взаимодействовать между собой по строго определенному стандарту. Технология разработки таких приложений кажется довольно сложной для тех, кто сталкивается с ней впервые. Трудности могут остаться надолго, если не уделить достаточно времени самым общим вопросам, то есть восприятию концепции СОМ (Модель многокомпонентных объектов). Поэтому не жалейте времени и пройдите через все, даже кажущиеся примитивными, этапы развития СОМ-приложений, как серверов, так и контейнеров. Мы начнем с того, что создадим СОМ-сервер с помощью сырых (raw) COM API-функций для того, чтобы лучше понять механизмы взаимодействия компонентов. Эти механизмы будут частично скрыты в следующих приложениях, которые мы будем развивать на основе стартовых заготовок, созданных мастером Studio.Net в рамках возможностей библиотеки шаблонов ATL (Active Template Library).
Любой программный продукт представляет собой набор данных и функций, которые как-то используют, обрабатывают эти данные. Этот принцип, как вы знаете, лежит в основе ООП. Там класс инкапсулирует данные и методы, которые служат для управления ими. Сходный принцип используется и в модели программирования СОМ. СОМ-объектом (или OLE-объектом) называется такой программный продукт, который обеспечивает доступ к данным с помощью одного или нескольких наборов функций, которые называются интерфейсами.
В отличие
от ООП, которое рассматривает интеграцию классов на уровне исходных модулей
— текстов программ, СОМ рассматривает интеграцию компонентов на двоичном уровне,
то есть на уровне исполняемых модулей. Цель — многократное использование удачно
разработанных компонентов именно на этом уровне. Двоичный уровень дает независимость
от аппаратной архитектуры и языков программирования (взамен порождая массу других
проблем). Двоичный стандарт взаимодействия позволяет СОМ-объектам, разработанным
разными поставщиками и на разных языках, эффективно взаимодействовать друг с
другом. С практической точки зрения СОМ — это набор системных библиотек (DLL-файлов),
которые дают возможность разным приложениям, выполненных с учетом требований
СОМ, взаимодействовать друг с другом. Исторически сложилось так, что СОМ состоит
из нескольких различных технологий, которые пользуются услугами друг друга для
формирования объектно-ориентированной системы. Каждая технология реализует определенный
набор функций.
Преимуществами
двоичных компонентов являются: взаимозаменяемость, возможность многократного
использования, возможность параллельной разработки с последующей
сборкой в одном проекте. Недостатки СОМ настолько очевидны, что я не буду их
перечислять. Вы почувствуете их в тот момент, когда начнете самостоятельно
разрабатывать свой первый СОМ-объект. Приведем далеко не полный список литературы,
который поможет более детально разобраться в технологии СОМ.
СОМ реализует
модель «клиент-сервер». Объекты, называемые серверами, предоставляют набор функций
в распоряжение других объектов, называемых клиентами, но СОМ-объект может быть
одновременно и клиентом, и сервером. Серверы всегда подчиняются спецификациям
СОМ, в то время как клиенты могут быть как СОМ-объектами, так и не быть таковыми.
Поставщик СОМ-объектов (сервер) делает объекты доступными, реализуя один или
множество интерфейсов. Пользователь СОМ-объектом (клиент) получает доступ к
объекту с помощью указателя на один или множество интерфейсов. С помощью указателя
клиент может пользоваться объектом, не зная даже как он реализован и где он
находится, но быть при этом уверенным, что объект всегда будет вести
себя одинаково. В этом смысле интерфейс объекта представляет собой некий контракт,
обещающий клиенту надежное поведение, несмотря на язык и местоположение клиента.
Благодаря этому решается проблема бесконечных обновлений версий сервера. Новая
версия сервера просто добавляет новые интерфейсы, но никогда не изменяет старых.
Клиент может либо пользоваться новым интерфейсом, если он о нем знает, либо
не пользоваться им, а вместо этого пользоваться старым. Добавление новых интерфейсов
никак не влияет на работу клиентов, работающих со старыми. Кроме того, как нас
уверяет документация, двоичный уровень делает компоненты независимыми от платформы
клиента.
Интерфейсы — основа СОМ-технологии
Разработчики СОМ не интересуются тем, как устроены компоненты внутри, но озабочены тем, как они представлены снаружи. Каждый компонент или объект СОМ рассматривается как набор свойств (данных) и методов (функций). Важно то, как пользователи СОМ-объектов смогут использовать заложенную в них функциональность. Эта функциональность разбивается на группы семантически связанных виртуальных функций, и каждая такая группа называется интерфейсом. Доступ к каждой функции осуществляется с помощью указателя на нее. В сущности, вся СОМ-технология базируется на использовании таблицы указателей на виртуальные функции (vtable).
Слово
interface (также как и слова object, element) становится перегруженным слишком
большим количеством смыслов, поэтому будьте внимательны. Интерфейсы СОМ — это
довольно строго определенное понятие, идентичное понятию структуры (частного
случая класса) в ООП, но ограниченное соглашениями о принципах его использования.
Каждый СОМ-компонент
может предоставлять клиенту несколько интерфейсов, то есть наборов функций.
Стандартное определение интерфейса описывает его как объект, имеющий таблицу
указателей на виртуальные функции (vtable). В файле заголовков BaseTyps.h, однако,
вы можете увидеть макроподстановку #def ine interface struct, которая показывает,
как воспринимает это ключевое слово компилятор языка C++. Для него интерфейс
— это структура (частный случай класса), но для разработчиков интерфейс отличается
от структуры тем, что в структуре они могут инкапсулировать как данные, так
и методы, а интерфейс по договоренности (by convention) должен содержать только
методы. Заметим, что компилятор C++ не будет возражать, если вы внутри интерфейса
все-таки декларируете какие-то данные.
Интерфейсы
придумали для предоставления (exhibition) клиентам чистой, голой (одной только)
функциональности. Существует договоренность называть все интерфейсы начиная
с заглавной буквы «I», например lUnknown, ZPropertyNotifySink и т. д. Каждый
интерфейс должен жить вечно и поэтому он именуется уникальным 128-битным идентификатором
(globally unique identifier), который в соответствии с конвенцией должен начинаться
с префикса IID_. Интерфейсы никогда нельзя изменять, усовершенствовать, так
как нарушается обратная совместимость. Вместо этого создают новые вечные интерфейсы.
Это
непреложное требование справедливо относят к недостаткам СОМ-техно-логии, так
как непрерывное усовершенствование компонентов влечет появление слишком большого
числа новых интерфейсов, зарегистрированных в вашем реестре. С проблемой предлагают
бороться весьма сомнительным образом — тщательным планированием компонентов.
Трудно, если вообще возможно, планировать в наше время (тем более рассчитывать
на вечную жизнь СОМ-объекта), когда сами информационные технологии появляются
и исчезают, как грибы в дождливый сезон.
Классы можно
производить от интерфейсов (и наоборот), а каждый интерфейс должен в
конечном счете происходить от интерфейса lUnknown. Поэтому все интерфейсы и
классы, производные от них, наследуют и реализуют функциональность lUnknown.
В связи с такой важностью и популярностью этого интерфейса рассмотрим его поближе.
Он определяет общую стратегию использования любого объекта СОМ:
interface lUnknown
{
public: virtual
HRESULT _stdcall Querylnterface(REFIID riid,
void **ppvObject)
= 0;
virtual ULONG _stdcall AddRef(void) = 0;
virtual ULONG _stdcall Release(void) = 0;
};
Как видите,
«неизвестный» содержит три чисто виртуальные функции и ни одного элемента данных.
Каждый новый интерфейс, который создает разработчик, должен иметь среди своих
предков I Unknown, а следовательно, он наследует все три указанных метода. Первый
метод Querylnterface представляет собой фундаментальный механизм, используемый
для получения доступа к желаемой функциональности СОМ-объекта. Он позволяет
получить указатель на существующий интерфейс или получить отказ, если интерфейс
отсутствует. Первый — входной параметр riid — содержит уникальную ссылку на
зарегистрированный идентификатор желаемого интерфейса. Это та уникальная, вечная
бирка (клеймо), которую конкретный интерфейс должен носить вечно. Второй — выходной
параметр — используется для записи по адресу ppvOb j ect адреса запрошенного
интерфейса или нуля в случае отказа. Дважды использованное слово адрес оправдывает
количество звездочек в типе void**. Тип возвращаемого значения HRESULT, обманчиво
относимый к семейству handle (дескриптор), представляет собой 32-битное иоле
данных, в котором кодируются признаки, рассмотренные нами в четвергом уроке.
Предположим,
вы хотите получить указатель на какой-либо произвольный интерфейс 1Му, уже зарегистрированный
системой и получивший уникальный идентификатор IID_IMY, с тем чтобы пользоваться
предоставляемыми им методами. Тогда следует действовать по одной из общепринятых
схем1:
//====== Указатель
на незнакомый объект
lUnknown *pUnk;
// Иногда приходит как параметр IМу *рМу;
// Указатель
на желаемый интерфейс
//====== Запрашиваем
его у объекта
HRESULT hr=pUnk->Query!nterfасе(IID_IMY,(void
**)&pMy);
if (FAILED(hr))
// Макрос, расшифровывающий HRESULT
{
//В случае неудачи
delete pMy;
// Освобождаем память
//====== Возвращаем
результат с причиной отказа
return hr;
else //В
случае успеха
//====== Используем
указатель для вызова методов:
pMy->SomeMethod();
pMy->Release();
// Освобождаем интерфейс
Возможна и другая
тактика:
//====== В случае
успеха (определяется макросом)
if (SUCCEEDED(hr))
{
//====== Используем
указатель
}
else
{
//====== Сообщаем
о неудаче
}
Второй параметр
функции Queryinterf асе (указатель на указатель) позволяет возвратить в вызывающую
функцию адрес запрашиваемого интерфейса. Примерная схема реализации метода Queryinterf
асе (в классе СОМ-объекта, производном от IМу) может иметь такой вид:
HRESULT _stdcall СМу::Queryinterfасе(REFIID id, void **ppv)
{
//=== В *ppv
надо записать адрес искомого интерфейса
//=== Пессимистический
прогноз (интерфейс не найден)
*ppv = 0;
// Допрашиваем REFIID искомого интерфейса. Если он
// нам не знаком,
то вернем отказ E_NOINTERFACE
// Если нас не знают, но хотят познакомиться,
// то возвращаем свой адрес, однако приведенный
// к типу "неизвестного" родителя
if (id
== IID_IUnknown)
*ppv = static_cast<IUnknown*>(this);
// Если знают, то возвращаем свой адрес приведенный
// к типу "известного" родителя IМу
else if (id
== IID_IMy)
*ppv = static_cast<IMy*>(this);
//===== Иначе
возвращаем отказ else
return E_NOINTERFACE;
//=== Если вопрос был корректным, то добавляем единицу
//=== к счетчику наших пользователей
AddRef();
return S_OK;
}
Методы AddRef
и Release управляют временем жизни объектов посредством подсчета ссылок (references)
на пользователей интерфейса. В соответствии с общей концепцией объект (или его
интерфейс) не может быть выгружен системой из памяти, пока не равен нулю счетчик
ссылок на его пользователей. При создании интерфейса в счетчик автоматически
заносится единица. Каждое обращение к AddRef
увеличивает счетчик на единицу, а каждое обращение к Release — уменьшает. При
обнулении счетчика объект уничтожает себя сам. Например, так:
ULONG СМу::Release()
{
//====== Если
есть пользователи интерфейса
if (—m_Ref
!= 0)
return m_Ref; // Возвращаем их число
delete this;
// Если нет —
уходим из жизни,
// освобождая память
return 0;
}
Вы, наверное,
заметили, что появилась переменная m_Ref. Ранее было сказано об отсутствии переменных
у интерфейсов. Интерфейсы — это голая функциональность. Но обратите внимание
на тот факт, что метод Release принадлежит не интерфейсу 1Му, а классу ему,
в котором переменные естественны. Обычно в классе СОМ-объекта и реализуются
чисто виртуальные методы всех интерфейсов, в том числе и главного интерфейса
zunknown. Класс ему обычно создает разработчик СОМ-объекта и производит его
от желаемого интерфейса, например, так:
class
СМу : public IMy
{
// Данные и методы
класса,
// в том числе и методы lUnknown
};
В свою очередь,
интерфейс IMy должен иметь какого-то родителя, может быть, только iUnknown,
а может быть одного из его потомков, например:
interface IMy : IClassFactory
{
// Методы интерфейса
};
СОМ-объектом
считается любой объект, поддерживающий хотя бы lUnknown. Историческое развитие
С ОМ-технологий определило многообразие терминов типа: OLE 94, OLE-2, OCX-96,
OLE Automation и т. д. Элементы ActiveX принадлежат к той же группе СОМ-объектов.
Каждый новый термин из этой серии подразумевает все более высокий уровень предоставляемых
интерфейсов. Элементы ActiveX должны как минимум обладать способностью к активизации
на месте, поддерживать OLE Automation, допуская чтение и запись своих свойств,
а также вызов своих методов.
Уникальная идентификация объектов
Данные
типа GUID (globally unique identifier) являются 128-битными идентификаторами,
состоящими из пяти групп шестнадцатеричных цифр,' которые обычно генерирует
специальная программа uuidgen, входящая в инструменты Studio.Net. Например,
если вы в командной строке Windows наберете
uuidgen -n2 -s
>guids.txt
то в файле
guids.txt получите два уникальных числа вида:
{12340001-4980-1920-6788-123456789012}
{1234*0002-4980-1920-6788-123456789012}
которые можно
использовать в качестве ключа регистрации в Windows-реестре. Рекомендуется обращаться
к утилите uuidgen и просить сразу много идентификаторов, а затем постепенно
использовать их (помечая, чтобы не забыть) в своем приложении для идентификации
интерфейсов, СОМ-классов и библиотек типов. Это упрощает отладку, поиск в реестре
и, возможно, его чистку. Кроме этого способа существуют и другие. Например,
можно обратиться к функции
HRESULT CoCreateGuid(GUID
*pguid);
которая гарантированно
выдаст уникальное 128-битное число, которое не совпадет ни с одним другим числом,
полученным в любой вычислительной системе, в любой точке планеты, в любое время
в прошлом и будущем. Впечатляюще, не правда ли? Есть целая серия функций вида
Uuid* из блока RFC-API, которые генерируют и обрабатывают числа типа GUID. Число,
как вы видите, разбито на пять групп, как-то связанных с процессом генерации,
в котором задействованы время генерации, географическое место, информация о
системе и т. д. Следующие типы переменных эквивалентны типу GUID:
Тип IID используется
также и для идентификации библиотек типов. Переменные типа GUID являются структурами,
содержащими четыре поля. Тип GUID определен в guiddef.h следующим образом:
typedef
struct
{
//=== 1-я группа
цифр (8 цифр - 4 байта)
unsigned long
Datal;
//=== 2-я группа
цифр (4 цифры - 2 байта)
unsigned short
Data2;
//=== 3-я группа
цифр (4 цифры - 2 байта)
unsigned short
Data3;
//=== 4-я и 5-я
группы (4 и 12 цифр) - 8 байт
byte Data4[8];
}
GUID;
Мы уже обсуждали
необходимость уникальной идентификации интерфейсов. Ну а зачем уникально идентифицировать
классы? Предположим, что два разработчика создали два разных СОМ-класса, но
оба назвали их MySuperGrid. Так как СОМ узнает класс по его CLSID, а алгоритм
генерации CLSID гарантирует его уникальность, то совпадение имен не мешает использовать
оба класса в одном клиентском приложении. Система пользуется двумя типами GUID:
строковым (применяется в реестре) и числовым (нужен клиентским приложениям).
Я думаю, что
в этот момент у неискушенного СОМ-технологией читателя должна слегка закружиться
голова. Это нормально, так как по заявлению авторитетов (David Cruglinsky),
она будет кружиться в течение примерно полугода, при условии регулярного изучения
СОМ-технологий.
Созданный
и подключенный компоновщиком динамически загружаемый модуль сервера система
интегрирует в пространство другого (клиентского) процесса, загрузив его по определенному
базовому адресу. Любая динамически загружаемая библиотека экспортирует функции,
которые пишутся в расчете на то, что их будет вызывать клиентское приложение
или другая DLL. Как только DLL спроецирована на адресное пространство вызывающего
процесса, ее данные и функции становятся доступными клиенту и представляют собой
просто дополнительный код и данные, как-то оказавшиеся в адресном пространстве
процесса.
СОМ-серверы,
которые хранятся в DLL-файлах, называются внутризадачными (in-process) серверами.
Но они могут быть реализованы и в виде ЕХЕ-файлов. Тогда они называются либо
локальными (local) серверами, либо удаленными (remote) серверами. Приложение-клиент
и локальный сервер функционируют в отдельных процессах или адресных пространствах
в рамках одной машины. Клиент и удаленный сервер функционируют не только в отдельных
процессах (адресных пространствах), но и разделены сетевыми каналами связи.
И тем и другим необходим коммуникационный мост, чтобы вызывать функции и передавать
друг другу данные. Такой мост обеспечивают библиотеки OLE, которые в качестве
средства реализации используют механизм RFC (Remote Procedure Call — удаленный
вызов процедуры). , Существует еще одна классификация СОМ или OLE-объектов.
В рамках MFC и поддерживаемой ею архитектуры документ — представление мы
можем создать объекты, которые либо поддерживают связь (linked) с приложением-контейнером,
либо внедрены в него (embedded). Некоторые приложения поддерживают как связывание,
так и внедрение объектов. Основное различие между двумя типами OLE-объектов
заключается в том, что источник данных внедренного (embedded) объекта является
частью документа контейнера и хранится вместе с данными контейнера, в
то время как данные связанного (linked) объекта хранятся в документе сервера,
то есть в файле, созданном и управляемым сервером. Объект контейнера, который
связан (linked), хранит лишь информацию, необходимую для связи с документом
сервера. Говорят, что объект контейнера хранит связь с документом сервера.
Приложение-сервер, поддерживающее связывание, должно уметь копировать
свои данные в буфер обмена для выполнения нужд контейнера по копированию объекта.
Обычно под внедренным объектом понимается обобщенный объект, независимо от способа
общения с ним (linked или embedded).
В конце этого урока мы (в рамках другой библиотеки — ATL) создадим DLL-сервер, который выполняет роль простейшего элемента ActiveX, внедряемого в окно приложения-клиента. Но сначала подробно рассмотрим, как взаимодействуют клиент и сервер в рамках приложения, использующего «сырые» (raw) функции COM API, с разработки которых и началось движение СОМ.
Сейчас мы
займемся разработкой DLL СОМ-сервера, выполняемого в пространстве процесса другого
(клиентского) приложения. Для того чтобы понять, что кроется за этой вывеской,
мы для начала создадим минимально-простой СОМ-объект и при этом специально не
будем пользоваться какими-либо библиотеками или инструментами Studio.Net.
Наш объект
будет предоставлять миру только один интерфейс isay, инкапсулирующий два метода:
Say и SetWord. Первый метод выводит текстовую строку типа BSTR в окно типа MessageBox,
а второй — позволяет изменять эту строку. Тип BSTR в Win32 является адресом
двухбайтовой Unicode-строки. Его советуют использовать в СОМ-объектах для обеспечения
совместимости с клиентскими приложениями, написанными на других языках.
Я надеюсь,
что логика, заложенная в этом простом приложении, поможет вам не терять нить
повествования при разработке следующего, более сложного объекта с помощью ATL.
Использование ATL и инструментов Studio.Net упрощают разработку СОМ-объектов,
но скрывают суть происходящего, вызывая иногда чувство досады и неудовлетворенности.
С помощью мастера AppWizard создайте шаблон приложения типа Win32 Dynamic-Link
Library (Динамически компонуемая библиотека Win32) под именем МуСот.
Это
же действие можно выполнить более сложным способом, но зато сход-ным с тем,
как это делалось в Visual Studio 6. Дайте команду File > New > File, выберите
тип файла и нажмите кнопку Open. Кроме этих действий придется записать новый
файл в папку с проектом и подключить его. Для этого используется команда Project
> Add Existing Item с последующим поиском файла. Альтернативой этому является
перетаскивание существующего файла в окне Solution Explorer из папки Resource
Files в папку Header Files.
//=== Эти директивы нужны для того, чтобы не допустить
//=== повторное
подключение файла
#if !defined(MY_ISAY_INTERFACE)
#define MY__ISAY_INTERFACE
#pragma once
//====== Для
того, чтобы были доступны COM API
#include <windows.h>
//====== Для
того, чтобы был виден lUnknown
#include <initguid.h>
// Интерфейс ISay мы собираемся зарегистрировать и
// показать миру. Он, как и положено, происходит от
// IUnknown и
содержит чисто виртуальные функции
interface ISay : public lUnknown
{
//=== 2 метода,
которые интерфейс
//=== предоставляет
своим клиентам
virtual HRESULT
_stdcall Say 0=0;
virtual HRESULT _stdcall SetWord (BSTR word)=0;
}
#endif
Абстрактный
интерфейс не может жить сам по себе. Он должен иметь класс-оболочку (wrapper
class), который на деле реализует виртуальные методы Say и SetWord. Этот так
называемый ко-класс (класс СОМ-компонента) производится от интерфейса ISay и
предоставляет тела всем унаследованным (чисто) виртуальным методам своего родителя.
Так как у интерфейса ISay, в свою очередь, имеется родитель (lUnknown), то класс
должен также дать реальные тела всем трем методам IUnknown.
Если
вы хотите, чтобы класс реализовывал несколько интерфейсов, то вы должны использовать
множественное наследование. Такой подход проповедует ATL (Active Template Library).
MFC реализует другой подход к реализации интерфейсов. Он использует вложенные
классы. Каждому интерфейсу соответствует новый класс, вложенный в один общий
класс СОМ-объекта.
Для того чтобы
быть доступным тем приложениям, которые захотят воспользоваться услугами СОМ-объекта,
сам класс тоже должен иметь дом (в виде inproc-сервера DLL). Сейчас, разрабатывая
проект типа Win32 DLL, мы строим именно этот дом. С помощью механизма DLL класс
будет доступен приложению-клиенту, в адресное пространство процесса которого
он загружается. Вы знаете, что DLL загружается в пространство клиента только
при необходимости.
Нам неоднократно
понадобятся услуги инструмента Studio.Net под именем GuidGen, поэтому целесообразно
ввести в меню Tools (Инструментальные средства) Studio.Net новую команду для
его запуска. GuidGen, так же как и UuidGen, умеет генерировать уникальные
128-битовые идентификаторы, но при этом он использует удобный Windows-интерфейс.
А идентификаторы понадобятся нам для регистрации сервера и класса CoSay. Для
введения новой команды:
// {170368DO-85BE-43af-AE71-053F506657A2}
DEFINE_GUID («name»,
0xl70368d0, 0x85be,
0x43af, 0xae, 0x71, 0x5, Ox3f, 0x50,
0x66, 0x57, Oxa2);
Замените аргумент
«name» на HD_ISay. Повторите всю процедуру и создайте идентификатор для ко-класса
CoSay, который вставьте сразу за идентификатором интерфейса ISay. На сей раз
замените аргумент «name» на CLSiD_CoSay, например:
// {9B865820-2FFA-lld5-98B4-OOE0293F01B2}
DEFINE_GUID(CLSID_CoSay,
0х9b865820, 0x2ffa,
0xlldS, 0x98, 0xb4, 0x0, 0xe0, 0x29,
0x3f, 0xl, 0xb2);
Сохраните
и закройте файл interfaces.h, так как мы больше не будем вносить в него изменений.
Если вы хотите знать, что делает макроподстановка DEFINE_GUID, то за ней стоит
такое определение:
#define DEFINE_GUID
(name, 1, wl, w2, \ b1, b2, bЗ, b4, b5, b6, b7, b8) \ EXTERN_C
const
GUID name \
= { 1, wl, w2,
{ b1, b2, bЗ,b4, b5, b6, b7, b8 } }
Оно означает,
что макрос создает структуру с именем <name> типа GUID, которая служит
для хранения уникальных идентификаторов СОМ-объектов, интерфейсов, библиотек
типов и других реалий причудливого мира СОМ.
Подключите
к проекту новый файл MyCom.h, в который надо поместить объявление класса CoSay.
Как вы помните, он должен быть потомком экспортируемого интерфейса iSay и дать
тела всем методам, унаследованным от всех своих абстрактных предков (isay, lUnknown).
Введите в файл следующие коды:
#if !defined(MY_COSAY_HEADER)
#define MY_COSAY_HEADER
#pragma once
class CoSay
: public ISay
{
//=====Класс,
реализующий интерфейсы ISay, lUnknown
public:
CoSay () ;
virtual -CoSay();
// lUnknown
HRESULT _stdcall
Querylnterface(REFIID riid, void** ppv);
ULONG _stdcall
AddRefO;
ULONG _stdcall
Release ();
// ISay
HRESULT _stdcall
Say();
HRESULT _stdcall
SetWord (BSTR word);
private:
//====== Счетчик
числа пользователей классом
ULONG m_ref;
, //====== Текст, выводимый в окно
BSTR m word;
};
#endif
Для реализации
тел методов класса CoSay подключите к проекту новый файл МуСоm. срр, в который
введите коды, приведенные ниже. Обратите внимание на то, как принято работать
со строками текста типа BSTR:
#include
"interfaces.h"
#include "MyCom.h"
//====== Произвольный
ограничитель длины строк
#define MAX_LENGTH
128
CoSay::CoSay()
{
//=== Обнуляем
счетчик числа пользователей класса,
//=== так как
интерфейс пока не используется
m_ref = 0;
//=== Динамически
создаем строку текста по умолчанию
m_word = SysAllocString
(L"Hi, there."
"This is MyCom
speaking");
}
CoSay::-CoSay()
{
//=== При завершении
работы освобождаем память
if (m_word)
SysFreeString(m_word);
}
//====== Реализация
методов lUnknown
HRESULT _stdcall CoSay::QueryInterface(REFIID riid, void** ppv)
{
//====== Стандартная
логика работы с клиентом
//====== Поддерживаем
только два интерфейса
*ppv = 0;
if (riid==IID_IUnknown)
*ppv = static_cast<IUnknown*>(this) ;
else if
(riid==IID_ISay)
*ppv = static_cast<ISay*>(this) ;
else
return E_NOINTERFACE;
//====== Есть
пользователи нашим объектом
AddRef();
return S_OK;
}
ULONG _stdcall CoSay:-.AddRef ()
{
return ++m_ref;
}
ULONG _stdcall CoSay::Release{)
{
if (--m_ref==0)
delete this;
return m_re f;
}
//====== Реализация
методов ISay
HRESULT _stdcall CoSay::Say()
{
//=== Преобразование
типов (из BSTR в char*), которое
//=== необходимо
для использования MessageBox
char buff[MAX_LENGTH];
WideCharToMultiByte(CP_ACP,
0, m_word, -1, buff, MAXJLENGTH, 0, 0);
MessageBox (0,
buff, "Interface ISay:", MB_OK);
return S_OK;
}
HRESULT _stdcall CoSay::SetWord(BSTR word)
{
//====== Повторное
выделение памяти
SysReAllocString
(&m_word, word);
freturn S_OK;
}
Класс, поддерживающий
интерфейс, готов. Теперь следует сделать доступным для пользователей СОМ-объекта
весь DLL-сервер, где живет ко-класс CoSay. Минимальным набором функций, которые
должна экспортировать COM DLL, является реализация только одной функции DllGetClassObject.
Обычно ее сопровождают еще три функции, но в данный момент мы рассматриваем
лишь минимальный набор. DLL должна создать СОМ-объект и позволить работать с
ним, получив, то есть записав по адресу ppv, адрес зарегистрированного интерфейса.
Вы, конечно, заметили, что в предложении дважды использовано слово адрес. Именно
поэтому параметр ppv имеет тип void** . Введите эту функцию в конец файла МуСот.срр:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)
{
//=== Если идентификатор класса задан неправильно,
if (rclsid
!= CLSID_CoSay)
// возвращаем код ошибки с указанием причины неудачи
return CLASS_E_CLASSNOTAVAILABLE;
//====== Создаем
объект ко-класса
CoSay *pSay =
new CoSay;
//=== Пытаемся получить адрес запрошенного интерфейса
HRESULT hr =
pSay->Query!nterface (riid, ppv) ;
if (FAILED(hr))
delete pSay;
return hr;
}
Макроподстановка
STDAPI при разворачивании превратится в
extern "С"
HRESULT stdcall
Работа
по опознаванию объектов идет с идентификаторами класса (rclsid) и интерфейса
(riid). Это является, как считают апологеты СОМ, одной из самых важных черт,
которые вносят небывалый уровень надежности в функционирование СОМ-приложений.
Весьма спорное утверждение, так как центром всей вселенной как разработчика,
так и пользователя становится Windows-реестр, который открыт всем ветрам — как
случайным, так и преднамеренным воздействиям со стороны человека и программы.
Однако следует согласиться с тем, что уникальная идентификация снимает проблему
случайного, но весьма вероятного совпадения имен интерфейсов, разработанных
в разных частях света. То же относится и к именам классов, библиотек типов и
т. д.
Для успешной
работы DLL следует добавить к проекту файл ее описания (DEF-файл). Этот способ
является альтернативным и, возможно, более простым, чем использование описателей
_declspec(dllexport) для экспортируемых функций.
DEF-файл сопровождает
DLL и содержит список функций, экспортируемых ею. Создайте новый файл MyCom.def
и введите в него такие строки:
LIBRARY "MYCOM.dll"
EXPORTS DllGetClassObject
PRIVATE
Заметим, что
теперь нет необходимости нумеровать экспортируемые функции, как это делалось
ранее (например, в рамках Visual Studio 6). Там вы должны были бы задать:
DllGetClassObject
@1 PRIVATE
При наличии
DEF-файла компоновщик создает (кроме основного файла библиотеки MyCom.dll) еще
два необходимых файла: MyCom.lib (заголовков экспортируемых функций) и МуСот.ехр
(информации об экспортируемых функциях и классах). При отсутствии последних
двух файлов система не сможет обратиться к функции DllGetClassObject, а следовательно,
и к нашему СОМ-объекту CoSay. Для того чтобы DEF-файл участвовал в процессе
сборки DLL, в рамках Visual Studio 6 его достаточно было лишь подключить к проекту.
Этого шага, однако, недостаточно в рамках Studio.Net. Надо сделать такую установку:
Следующим
шагом вы должны зарегистрировать сервер, то есть внести в реестр Windows записи,
которые регистрируют факт существования и местоположение DLL. При работе с ATL
это действие будет автоматизировано, но сейчас создайте и подключите к проекту
еще один файл MyCom.reg, формат которого соответствует командам регистрации,
воспринимаемым редактором реестра RegEdit.exe. При этом вам, вероятна, придется
действовать альтернативным способом, описанным выше. По крайней мере в бета-версии
Studio.Net, с которой я имею дело, в списке типов добавляемых файлов отсутствует
тип REG. В текст, приведенный ниже, вы должны подставить идентификаторы, соответствующие
вашей регистрации, а также ваш путь к файлу MyCom.dll:
REGEDIT
HKEY_CLASSES_ROOT\MyCom.CoSay\CLSID =
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}
HKEY_CLASSES_ROOT\CLSID\
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}
= MyCom.CoSay
HKEY_CLASSES_ROOT\CLSID\
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}
\InprocServer32
= D:\MyCom\Debug\MyCom.dll
Обратите внимание на то, что текст каждой из трех команд не должен разрываться символами перехода на другую строку. В книге мы вынуждены делать переносы, которых не должно быть в вашем файле. Сохраните и закройте файл. Теперь для регистрации сервера и вложенного в него класса СОМ-объекта надо дважды щелкнуть по имени файла MyCom.reg в окне Windows File Manager или Windows Explorer и согласиться с реакцией системы типа «Вы действительно хотите...» После этого соберите проект, дав команду Build > Build. Процесс сборки должен пройти без ошибок. Теперь наш простейший DLL СОМ-сервер зарегистрирован и готов к использованию.
Разработка клиентского приложения
Для разработки
минимального приложения, способного найти DLL COM inproc-сервер, можно начать
с заготовки простого приложения консольного типа, инициализировать системные
COM DLL и обратиться к ним с просьбой найти наш СОМ-объект и загрузить DLL в
адресное пространство нашего процесса. Все это делается при вызове функции CoGetclassObject
из семейства сом API. Обратите внимание на то, что нам не надо изменять настройки
проекта (Project > Settings) и указывать компоновщику на необходимость
подключения DLL, а также указывать ее локальный или сетевой адрес. Собственно,
в этом и есть главная заслуга СОМ. Приложение-клиент можно перенести на другую
машину, и если там зарегистрирован наш СОМ-объект, то он будет найден и правильно
загружен. Функция CoGetclassObject одновременно с поиском и загрузкой DLL СОМ-серве-ра
возвращает адрес запрошенного интерфейса. В нашем случае — это isay. Имея адрес
интерфейса, можно обращаться к его методам, управляя, таким образом, объектом.
#include
"interfaces.h"
void main ()
{
//====== Инциализация
COM Library
Colnitialize(0);
//====== Сюда
хотим записать адрес интерфейса
ISay *pSay;
// Пытаемся найти и загрузить СОМ DLL-сервер, а также
// получить адрес
вложенного интерфейса, указав
// два уникальных
идентификатора CLSID_CoSay и IID_ISay
HRESULT hr =
CoGetClassObject (CLSID_CoSay,
CLSCTX_INPROC_SERVER,
0, IID_ISay, (void**)&pSay);
if (FAILED(hr))
{
MessageBox(0,"Could not get class object!
", "CoGetClassObject",MB_OK);
CoUninitialize();
return;
}
//====== В случае
успеха командуем объектом
pSay->Say();
BSTR word = SysAllocString(L"I
hear you well");
pSay->SetWord(word);
SysFreeString(word);
pSay->Say();
//====== Освобождаем
интерфейс
pSay->Release();
//====== Закрываем
и выгружаем COM Library
CoUninitialize();
}
Запустите приложение (Ctrl+F5), и если вы не допустили какой-либо неточности, то должны увидеть окно сообщения со строкой Hi, there.... После нажатия клавиши Enter должно появиться другое окно с текстом I hear you well. Этот текст задан клиентским приложением, а воспринят и воспроизведен СОМ-объектом. Если объект не работает, то терпеливо проверьте все этапы создания сервера. В модели СОМ существует довольно много мест, где можно допустить ошибку. Наиболее вероятны ошибки в процессе регистрации.
Логика функционирования нашего проекта (типа клиент-сервер ) вырождена, то есть излишне упрощена, так как мы хотели показать лишь основную нить алгоритма использования СОМ-объектов. Обычно в рамках этого алгоритма присутствует так называемая фабрика классов — специальный класс на стороне сервера, который реализует функциональность уже существующего и зарегистрированного в библиотеке СОМ интерфейса iciassFactory. Фабрики классов — это объекты СОМ создающие другие объекты сервера. Их цель — создать объект определенного типа, который однозначно задан своим CLSID. Каждый СОМ-объект должен в соответствии со стандартом иметь связанную с ним фабрику классов, которая ответственна за его создание. Так, в нашем случае мы должны иметь фабрику классов, способную воспроизводить любое требуемое клиентами количество объектов класса CoSay.
Интерфейс iciassFactory имеет всего два метода: Createlnstance и LockServer. Первый необходим для того, чтобы динамически создавать произвольное количество объектов тех классов (CLSID), которые живут в доме DLL СОМ-сервера, а второй — для того, чтобы запретить или разрешить системе выгружать сервер из памяти. Это позволяет пользователю гибко управлять необходимыми ресурсами. Если СОМ-объект пока не нужен клиентскому приложению, но вскоре может понадобиться, то, вызвав метод LockServer с параметром TRUE, клиент может запретить выгрузку из памяти DLL-сервера, несмотря на то что счетчик числа пользователей ее объектами равен нулю. Если в течение какого-то времени не предвидится использование СОМ-объектов, то клиент может вызвать метод LockServer с параметром FALSE, разрешив тем самым выгрузку DLL-сервера из памяти.
Для реализации этой функциональности вновь откройте проект СОМ-сервера My с от и в файл МуСоm.срр добавьте две глобальные переменные:
//====== Счетчик числа блокировок DLL
ULONG gLockCount;
//====== Счетчик
числа пользователей СОМ-объектами
ULONG gObjCount;
В этот же
файл введите новую функцию, которую будет экспортировать наша DLL:
STDAPI DllCanUnloadNow()
{
//====== Если
счетчики нулевые, то мы позволяем
//====== системе
выгрузку DLL-сервера
return !gLockCount && IgObjCount ? S_OK : S_FALSE;
}
В конструктор
класса coSay добавьте код, увеличивающий счетчик числа пользователей объектом
Со Say:
gObjCount++;
а в деструктор
— уменьшающий:
gObjCount--;
Важным шагом, о котором, тем не менее, легко забыть, является своевременная коррекция файла MyCom.def. Вставьте в конец этого файла строку
DllCanUnloadNow
PRIVATE
которая добавляет в список экспортируемых функций еще один элемент. В файл MyCom. h добавьте декларацию нового класса CoSayFactory, реализующего интерфейс iclassFactory. Отметьте, что он произведен от интерфейса iClassFactory, который, как и положено, имеет родителя I unknown. Вы помните, что на плечи класса ложится бремя реализации всех методов своих предков. По той же причине мы вновь заводим счетчик числа пользователей классом (m_ref):
//====== Фабрика
классов СОМ DLL-сервера
class CoSayFactory : public IClassFactory
{
public:
CoSayFactory()
;
virtual ~CoSayFactory()
;
// lUnknown
HRESULT _stdcall
Querylnterface(REFIID riid,
void** ppv);
UbONG _stdcall
AddRefO; ULONG _stdcall Release();
// IClassFactory
HRESULT _stdcall
Createlnstance(LPUNKNOWN pUnk,
REFIID riid,
void** ppv);
HRESULT _stdcall
LockServer(BOOL bLock); private:
ULONG m_ref;
};
Реализацию
тел заявленных методов вставьте в файл МуСоm.срр. Здесь мы вынуждены повторяться,
вновь прокручивая логику управления временем жизни объектов СОМ:
//==========
Фабрика классов
CoSayFactory::CoSayFactory()
{
m_ref = 0; gObjCount++;
}
CoSayFactory::-CoSayFactory()
{
gObjCount--;
}
//====== Методы
lUnknown
HRESULT _stdcall CoSayFactory
::QueryInterface(REFIID riid, void** ppv)
{
*ppv = 0;
//=== На сей
раз обойдемся без шаблона static_cast<>
if (riid
== IID_IUnknown)
*ppv = (lUnknown*)this;
else if (riid
== IID_IClassFactory)
*ppv = (IClassFactory*)this;
else
return E_NOINTERFACE;
AddRef();
return S_OK;
}
ULONG _stdcall CoSayFactory:rAddRef()
{
return ++m_ref;
}
ULONG _stdcall CoSayFactory::Release()
{
if (--m_ref==0)
delete this;
return m_ref;
//====== Методы
интерфейса IClassFactory
HRESULT _ stdcall
CoSayFactory: :CreateInstance
(LPUNKNOWN pUnk, REFIID riid, void** ppv)
{
// Этот параметр управляет аггрегированием
// объектов СОМ,
которое мы не поддерживаем
if (pUnk)
return CLASS_E_NOAGGREGATION;
//== Создание нового объекта и запрос его интерфейса
CoSay *pSay =
new CoSay;
HRESULT hr = pSay->Query!nterface (riid, ppv) ;
if (FAILED
(hr))
delete pSay;
return hr;
//=== Управление счетчиком фиксаций сервера в памяти
HRESULT _stdcall CoSayFactory::LockServer(BOOL bLock)
{
if (bLock)
// Если TRUE, то увеличиваем счетчик
++gLockCount;
else //
Иначе — уменьшаем
--gLockCount;
return S_OK;
}
Мы должны
также изменить алгоритм функции DllGetciassOb j ect, которая теперь создает
объект фабрики классов и запрашивает один из двух возможных интерфейсов (lUnknown,
IClassFactory):
STDAPI DllGetClassObject (REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
if (rclsid
!= CLSID_CoSay)
return CLASS_E_CLASSNOTAVAILABLE;
CoSayFactory *pCF = new CoSayFactory;
HRESULT hr =
pCF->Query!nterface(riid, ppv);
if (FAILED(hr))
delete pCF;
return hr;
}
На этом модификация
сервера завершается. Дайте команду Build > Rebuild и устраните ошибки, если
они имеются. Затем вновь откройте проект клиентского приложения SayClient и
внесите изменения в функцию main, которая теперь должна работать с объектами
СОМ более изощренным способом. Она должна сначала загрузить
СОМ-сервер и запросить адрес его фабрики классов, затем создать с ее помощью
объект CoSay, попросив у него адрес интерфейса isay, и лишь после этого можно
начать управление объектом. Последовательность освобождения объектов тоже должна
быть тщательно выверена. Ниже приведена новая версия файла SayClient.cpp:
#include
"interfaces.h"
void main()
{
(reinitialize
(0) ;
IClassFactory
*pCF;
// Мы зарегистрировали
только один класс CoSay,
// поэтому ищем
DLL с его помощью, но при этом
// создается
не объект CoSay, а объект CoSayFactory
// (см. код функции
DllGetClassObject).
// Поэтому здесь
мы просим дать адрес
// интерфейса
IClassFactory
HRESULT hr =
CoGetClassObject(CLSID_CoSay, CLSCTX_INPROC_SERVER,0, IID_IClassFactory,(void**)&pCF);
if (FAILED(hr))
{
MessageBox(0,"Could not Get Class Factory !
", "CoGetClassObject",
MB_OK);
CoUninitialize();
return;
}
// Далее мы с
помощью фабрики классов
// создаем объект
CoSay и просим его
// дать нам адрес
интерфеса ISay
ISay *pSay;
hr = pCF->Create!nstance(0,IID_ISay,
(void**)&pSay) ;
if (FAILED(hr))
{
MessageBox(0,"Could not create CoSay and get ISay!
", "Createlnstance",
MB_OK);
CoUninitialize
();
return;
}
// Уменьшаем счетчик числа пользователей
// фабрикой классов
pCF->Release();
//====== Управляем
объектом
pSay->Say();
BSTR word = SysAllocString(L"Yes,
My Lord");
pSay->SetWord(word);
SysFreeString(word);
pSay->Say();
//====== Уменьшаем
число его пользователей
pSay->Release();
SCoUninitialize () ;
}
Запустите
приложение (Ctrl+F5) и проверьте его работу. Алгоритм проверки остается тем
же, что и ранее, но здесь мы должны по логике разработчиков СОМ, радоваться
тому, что выполняем большее число правил и стандартов, а также имеем возможность
одновременно создавать несколько СОМ-объектов.
На
мой взгляд, не может быть ничего лучшего, чем получить код хорошо продуманного
класса C++, который дает вам новую, хорошо документированную функциональность.
При этом вы получаете полную свободу в том, как ее использовать, и имеете возможность
развивать ее по вашему усмотрению. Использование методов класса предполагает
выполнение оговоренных заранее правил игры, так же как и при использовании методов
интерфейсов. Но эти правила значительно более естественные, чем правила СОМ.
Вы, возможно, возразите, что для внедрения в проект нового класса, сам проект
надо строить заново. Двоичный объект СОМ в этом смысле внедрить проще. Но здесь
надо учитывать тот факт, что для реализации всех выгод СОМ вам придется разработать
универсальный контейнер объектов, который будет способен внедрять СОМ-объекты
будущих поколений и управлять ими. Это невозможно сделать, не трогая кода вашего
приложения. Разработчик более или менее серьезного проекта постоянно корректирует
его, изменяя код того или иного модуля. Он просто обречен на это. На мой взгляд,
при реализации новых идей проще использовать исходные коды классов, чем двоичные
объекты. Без сомнения, за хорошие коды надо платить, также как и за хорошие
СОМ-объекты.
Разработанный
DLL СОМ-сервер выполняет свою функцию, обслуживая клиентское приложение, разработанное
на языке C++. Но он не будет работать с приложениями, написанными на других
языках. В MS-документации под другими языками имеют в виду СОМ-совместимые языки:
VB, VBScript, Visual J++ и С в версии Microsoft. Остальные платформы и языки
пренебрегают технологией СОМ и поэтому как бы не существуют.
Так вот, чтобы
сделать наш объект доступным из клиентского приложения, разработанного на одном
из перечисленных четырех языков, надо познакомиться с еще одним внушительным
пластом технологии СОМ. Это язык MIDL (Microsoft Interface Definition Language)
и компилятор этого языка (MIDL compiler), который тоже иногда называют просто
MIDL. Язык MIDL имеет достаточно много новых для C++ ключевых слов, которые
более точно описывают атрибуты интерфейсов, классов и их методов, но он не имеет
никаких исполняемых операторов (типа for, if и т. д.). Предположим, что вы создали
файл MyCom.idl, в котором более точно описали интерфейсы, класс объекта СОМ
и библиотеку его типов. В результате компиляции вашего IDL-файла будут сгенерированы
несколько других файлов. В их число входят две заглушки MyCom_i.c и МуСот_р.с
на языке С и файл заголовков MyCom.h. Эти файлы теперь можно использовать для
обеспечения интерфейса между клиентским и серверным приложениями.
Все начиналось
с языка С, но потом было решено, что другие языки тоже должны участвовать в
движении СОМ. Проблема совместимости языков возникает потому, что типы данных,
используемые в языке С, не совпадают с типами в других языках. Более того, в
некоторых из этих языков переменная может по прихоти разработчика изменять свой
тип по ходу программы, что совершенно неприемлемо в логике С и C++. В связи
с этим и был разработан метаязык более высокого уровня, который используется
только для определений (definitions) всех данных, связанных с объектами СОМ,
и сопряжения их типов. MIDL пришел на смену языку ODL (Object Description Language)
и его компилятору MkTypeLib. Кроме тогЪ, вы можете встретить упоминания о стандарте
DCE RFC IDL (Distributed Computing Environment Remote Procedure Call Interface
Definition Language), который тоже устарел, так как не поддерживает определений,
связанных с объектами.
При использовании
технологий Microsoft вы всегда должны быть готовыми к тому, что для обозначения
тех же самых или слегка модифицированных понятий изобретаются абсолютно новые
термины, носящие, на мой взгляд, более рекламный, чем смысловой характер. Делая
заплату на какие-то явные (или не очень) промахи, целесообразно представить
ее в виде новой, даже революционной, технологии, так как этот факт повышает
marketability (конкурентоспособность). Но для разработчика это означает лишь
дополнительные усилия на выделение истинной сути новшеств и поиск тождественных
или сходных понятий, без которых трудно выстроить более или менее стройную модель
или структуру, призванную помогать в разработке приложений.
СОМ спроектирован
так, чтобы обеспечить прозрачную (transparent) коммуникацию клиента с сервером
независимо от того, где они находятся:
С точки зрения
клиента все СОМ-объекты управляются одинаковым способом — с помощью указателя
на интерфейс, который должен действовать в адресном пространстве клиента. Если
СОМ-объект находится в этом же пространстве, то вызов метода какого-либо из
его интерфейсов осуществляется прямо, без посредников. Если объект расположен
вне рамок клиентского процесса, то вызов осуществляется с помощью посредников,
называемых заглушками. Их либо автоматически генерирует СОМ, либо создает сам
разработчик.
С точки зрения
сервера все вызовы также осуществляются с помощью указателя на интерфейс. Но
теперь указатель должен действовать в контексте процесса серверного приложения.
Если процессы совпадают (inproc-server), то можно обойтись без заглушек, но
если нет, то нужен еще один посредник, который расположен в пространстве серверного
процесса.
Для того чтобы
клиент, написанный на любом из перечисленных (элитных) языков, мог вызвать метод
интерфейса из СОМ-объекта, расположенного в рамках другого процесса, несколько
компонентов должны объединить свои усилия. Прежде всего это две заглушки (клиентская
и серверная). В технологии RPC (Remote Procedure Call) они так и называются.
В СОМ клиентская заглушка называется proxy stub, или просто proxy (представитель
интересов сервера).
Когда клиент
вызывает метод локального или удаленного сервера (рис. 8.1), этот вызов перехватывается
представителем настоящего сервера, расположенным в адресном пространстве клиента
(proxy). Последний получает запрос на вызов метода, упаковывает параметры, которые
будут посланы серверу, и вызывает соответствующий метод при помощи RPC. Акт
передачи данных, то есть параметров функций и возвращаемых значений, за пределы
процесса называется транспортировкой. Она включает в себя упаковку, передачу
и распаковку данных по достижении ими места назначения. Отметьте, что транспортировать
надо как данные, так и интерфейсные указатели.
С другой стороны,
специальная часть кода на сервере (stub), получает от proxy запрос на вызов
метода, распаковывает параметры и вызывает нужный метод реального сервера. Сервер,
выполнив клиентский запрос, обычно возвращает какие-то данные. Посредник на
стороне сервера (stub) перехватывает эти данные, упаковывает их и направляет
соответствующему посреднику на стороне клиента (proxy). Последний получает возвращаемые
данные, распаковывает их и передает клиенту. Библиотеки СОМ автоматически обеспечивают
код функций proxy/ stub для стандартных интерфейсов. При написании же собственных
интерфейсов следует пользоваться интерфейсом, производным от iMarshal. Итак,
заместитель расположен в адресном пространстве клиента и представляет интересы
СОМ-объекта на стороне клиента, обеспечивая суррогатные точки входа для каждого
из методов, обозначенных в исходном IDL-файле. Когда клиент делает вызов удаленной
(remote) процедуры сервера, то сначала он вызывает суррогат этой процедуры в
заглушке proxy (в пространстве своего процесса). Последняя осуществляет:
Серверная
заглушка, или просто stub, распаковывает (unmarshals) параметры и передает их
объекту СОМ. Она также запаковывает ответную информацию, возвращаемые параметры,
для того чтобы передать их назад клиенту.
Описанные
действия называются маршализацией аргументов. Эта процедура сильно зависит от
типа параметров. Например, маршализация массива данных значительно сложнее маршализации
переменной целого типа или указателя на структуру. Для каждого типа данных существуют
свои отдельные функции. Proxy состоит из части, которая размещена в OLE32. DLL
(proxy manager), и частей, которые зависят от интерфейсов СОМ-объекта (interface
proxies). Для клиента proxy представляет собой реальный СОМ-объект.
Сам канал
передачи обслуживается функциями библиотеки СОМ. Канал передает буфер (с маршализованными
параметрами) во владение функциям из RPC-библиотеки, которые и занимаются его
передачей через границу между процессами. Вы можете выбирать между стандартной
маршализацией, обеспечиваемой библиотекой СОМ, и своей собственной (custom marshaling).
В последнем случае вы должны разработать интерфейс, производный от IMarshal.
Каждый отдельный интерфейс может пользоваться одним из двух способов маршализации
своих параметров. Это определяется на этапе проектирования СОМ-класса, реализующего
интерфейсы. Здесь уместно привести схему, которую вы также можете увидеть в
MSDN (Search > Marshaling Details).
Рис.
8.1. Схема коммуникации клиент-сервер
СОМ не накладывает
ограничений на структуру компонентов, он определяет лишь порядок их взаимодействия.
В основе межпроцессной коммуникации лежит все та же косвенная адресация (таблица
виртуальных функций), которая позволяет передать управление либо прямо методу
интерфейса (inproc-server), либо его представителю (proxy) на стороне клиента,
который, в свою очередь, делает RPC (удаленный вызов) метода настоящего объекта.
Прозрачность СОМ-объекта для клиента заключается в том, что proxy-объект знает,
где расположен реальный объект (на другом компьютере — remote server, или на
том же самом — local server), а клиент об этом не знает.
Когда клиент
хочет использовать СОМ-сервер, он обращается к системной библиотеке с просьбой
найти и загрузить сервер, чей CLSID равен определенному значению. Заодно клиент
передает IID требуемого интерфейса. В ответ на это системная COM DLL вызывает
SCM (Service Control Manager) — менеджер сервисов, который запускается во время
загрузки системы. SCM является RFC-сервером, который использует системный реестр,
чтобы выполнить поиск реализации, то есть отыскать ЕХЕ- или DLL-файл, содержащий
требуемый СОМ-сервер. Чтобы найти модуль сервера, SCM ищет в реестре его CLSID.
Если он найден, то SCM возвращает связанный с ним файловый путь, а СОМ загружают
этот модуль в память. Теперь возможны два варианта действий: если сервер находится
в ЕХЕ-файле, то СОМ запускает его, устанавливает канал связи (RPC) между представителями
клиента и сервера (proxy/stub) и возвращает интерфейсный указатель клиенту.
Если СОМ-сервер находится в DLL-файле, СОМ просто передаст клиенту указатель
на фабрику классов сервера.
Для того чтобы
клиенты, разработанные на других языках программирования, могли управлять объектами
сервера, они должны иметь информацию о типах данных, используемых сервером при
передаче параметров. Одним из способов получения этой информации является создание
сервером библиотеки типов. Возвращаясь к файлам, которые сгенерировал компилятор
MIDL, отметим, что он создает еще один (двоичный) TLB-файл (Type Library). После
успешной компиляции вы можете обнаружить его в папке Debug. COM использует этот
файл для реализации маршалинга, управляемого данными, который происходит на
этапе выполнения программы. Двоичный TLB-файл воспринимается клиентом, написанным
на одном из СОМ-совместимых языков. Например, его использует программа просмотра
объектов Microsoft Excel. Инструмент Studio.Net ClassWizard умеет по информации
из библиотеки типов создать классы, которые могут обращаться к свойствам и методам
объектов. Программа на Visual Basic осуществляет раннее связывание на основе
данных из библиотеки типов. Сведения о библиотеке типов также заносятся в реестр
в специальный подраздел TypeLib в разделе HKEY_CLASSES_ROOT.
Для ознакомления
с возможностями MIDL создайте новый пустой проект типа Win32 DLL. Для этого:
//====== Импорт
библиотечных определений
import "oaidl.idl";
import "ocidl.idl";
//====== Уточненное
описание интерфейса ISay
[
object, uuid(170368DO-85BE-43af-AE71-053F506657A2)
,
helpstring("My Test DLL COM-server ISay")
]
interface ISay : lUnknown
{
HRESULT Say();
HRESULT SetWord([in]BSTR word);
}
//====== Описание
библиотеки типов
[
uuid(0934DA90-608D-4107-9ECC-C7E828AD0928),
version(1.0),
helpstring("My Test DLL COM-server Type Library")
]
library MyCom
{
importlib("stdole32.tlb")
;
[uuid(9B865820-2FFA-lld5-98B4-OOE0293F01B2)]
//====== Описание
класса реализации интерфейса
coclass CoSay
{
[default] interface ISay; };
};
Попробуйте
откомпилировать новый файл описания интерфейса, используя клавиатурную комбинацию
Ctrl+F7. Если на этом этапе возникнут ошибки, то проверьте настройку проекта
View > Property Pages > MIDL > General > MkTy ре Lib
Compatible (она должна быть в состоянии No) и повторите компиляцию. После успешного
ее завершения просмотрите содержимое папки проекта. В ней должны появиться новые
файлы: MyComTLib_h.h, MyComTLibJ.c, MyComTLib_p.c и dlldata.c. Эти файлы, как
было сказано, помогают обеспечить взаимодействие клиента с сервером. В результате
их компиляции и сборки будет сгенерирована DLL, в которой реализованы коды заглушек
proxy/stub.
Для того чтобы
двинуться дальше, вам необходимо взять некоторые файлы из папки МуСот с предыдущим
проектом типа DLL.
MyComTLib.def
: Declares the module parameters. LIBRARY "MYCOMTLIB.dll"
EXPORTS .
DllGetclassObject PRIVATE
DllCanUnloadNow PRIVATE
Разработчики COM рекомендуют для повышения надежности и переносимости компонентов использовать при их разработке множество макроопределений, которые вы также вынуждены будете использовать при разработке проекта на базе ATL. Например, макрос STDMETHODIMP при раскрытии заменяет спецификаторы HRESULT _stdcall. Для того чтобы приобрести навыки использования макросов СОМ, мы применим их в файлах MyCom.h и MyCom.cpp. Сравнивая старую и новую версии этих файлов, вы без труда поймете смысл макроподстановок. В файл MyCom.h ведите коррекцию кодов так, как показано ниже:
#if !defined(MY_COSAY_HEADER)
#define MY_COSAY_HEADER
#pragma once
#include "MyComTLib_h.h"
class CoSay : public ISay
//====== Класс,
реализующий интерфейсы ISay, lUnknown
public: CoSay (') ;
virtual -CoSay();
// lUnknown
STDMETHODIMP
QuerylnterfacefREFIID riid, void** ppv);
STDMETHODIMP_(ULONG)
AddRef();
STDMETHODIMP_(ULONG)
Release();
// ISay
STDMETHODIMP
Say();
STDMETHODIMP
SetWord (BSTR word);
private:
//====== Счетчик
числа пользователей классом
ULONG m_ref;
//====== Текст,
выводимый в окно
BSTR m_word;
};
//====== Фабрика
классов СОМ DLL-сервера
class CoSayFactory
: public IClassFactory
{
public:
CoSayFactory();
virtual ~CoSayFactory();
// lUnknown
STDMETHODIMP
QueryInterface(REFIID riid, void** ppv) ;
STDMETHODIMP_(ULONG)
AddRef();
STDMETHODIMP_(ULONG)
Release();
// IClassFactory
STDMETHODIMP
Createlnstance(LPUNKNOWN pUnk,
REFIID riid,
void** ppv);
STDMETHODIMP LockServer(BOOL bLock);
private:
ULONG m_ref;
};
#endif
Теперь перейдите
к файлу MyCom.cpp и произведите замены в соответствии с текстом, приведенным
ниже:
#include "MyComTLib_i.c"
#include "MyCom.h"
//====== Произвольный
ограничитель длины строк
#define
MAX_LENGTH 128
//====== Счетчик
числа блокировок DLL
ULONG gLockCount;
//====== Счетчик
числа пользователей СОМ-объектами
ULONG gObjCount;
CoSay::CoSay()
{
//=== Обнуляем
счетчик числа пользователей класса,
//=== так
как интерфейс пока не используется
m_ref = 0;
//=== Динамически создаем строку текста по умолчанию
m_word = SysAllocString(L"This is MyComTLib speaking");
gObjCount++;
}
CoSay::-CoSay()
{
//====== при
завершении работы освобождаем память
if (m_word)
SysFreeString(m_word);
gObjCount—;
}
//====== Реализация
методов lUnknown
STDMETHODIMP CoSay::QueryInterface(REFIID riid, void** ppv)
{
// Стандартная
логика работы с клиентом
// Поддерживаем
только два интерфейса
//====== Реализация
lUnknown *ppv = 0;
if (riid==IID_IUnknown)
*ppv = static_cast<IUnknown*>(this);
else if (riid==IID_ISay)
*ppv = static_cast<ISay*>(this);
else
return E_NOINTERFACE;
//====== Добавляем
единицу к счетчику
//====== пользователей
нашим объектом
AddRef () ;
return S_OK;
}
STDMETHODIMP_(ULONG) CoSay::AddRef()
{
return ++m_ref;
}
STDMETHODIMP_(ULONG) CoSay: :Release ()
{
if (--m_ref==0)
delete this;
return m_ref;
}
//====== Реализация
ISay
STDMETHODIMP CoSay::Say()
{
//=== Преобразование типов (из BSTR в char*),
//=== которое необходимо для использования
MessageBox char
buff[MAX_LENGTH];
WideCharToMultiByte(CP_ACP, 0, m_word, -1,
buff, MAX_LENGTH, 0, 0);
MessageBox (0, buff, "Interface ISay:", MB_OK);
return S_OK;
}
STDMETHODIMP CoSay::SetWord(BSTR word)
{
//====== Повторное
зыделение памяти
SysReAllocString(&m_word,
word);
return S_OK;
}
STDAPI DllGetClassObject (REFCLSID rclsid,
REFIID riid, LPVOID* ppv)
{
if (rclsid
!= CLSID_CoSay)
return CLASS_E_CLASSNOTAVAILABLE;
CoSayFactory
*pCF = new CoSayFactory;
HRESULT hr =
pCF->Query!nterface(riid, ppv);
if (FAILED(hr))
delete pCF;
return hr;
}
STDAPI DllCanUnloadNow()
{
//====== Если
счетчики нулевые, то мы позволяем
//====== системе
выгрузку DLL-сервера
return IgLockCount && IgObjCount ? S_OK : S_FALSE;
}
//====== Фабрика
классов
CoSayFactory::CoSayFactory()
{
m_ref = 0;
gObjCount++;
}
CoSayFactory::-CoSayFactory()
gObjCount--;
}
//====== Методы
lUnknown
STDMETHODIMP CoSayFactory
::QueryInterface(REFIID riid, void** ppv)
{
*ppv = 0;
//=== Обходимся
без шаблона static casto
if (riid
== IID_IUnknown)
*ppv = (lUnknown*)this;
else if (riid
== IID_IClassFactory)
*ppv = (IClassFactory*)this;
else
return E_NOINTERFACE;
AddRef () ;
return S_OK;
}
STDMETHODIMP_(ULONG) CoSayFactory::AddRef()
{
return ++m_ref;
}
STDMETHODIMP_(ULONG) CoSayFactory::Release()
{
if (--m_ref==0)
delete this;
return m_ref;
}
//====== Методы
IClassFactory
STDMETHODIMP
CoSayFactory::CreateInstance(LPUNKNOWN pUnk,
REFIID riid, void** ppv)
{
// Этот параметр управляет аггрегированием объектов СОМ,
// которое мы
не поддерживаем if (pUnk)
return CLASS_E_NOAGGREGATION;
//=== Создание нового объекта и запрос его интерфейса
CoSay *pSay =
new CoSay;
HRESULT hr = pSay->Query!nterface (riid, ppv);
if (FAILED(hr))
delete pSay;
return hr;
}
//=== Управление счетчиком фиксаций сервера в памяти
STDMETHODIMP CoSayFactory::LockServer(BOOL bLock)
{
if (bLock) // Если TRUE, то увеличиваем счетчик
++gLockCount;
else //
Иначе — уменьшаем
--gLockCount; return S_OK;
}
Регистрация
библиотеки типов
Библиотеку
типов также надо регистрировать для того, чтобы клиент мог найти ее с помощью
уникального идентификатора. Введите изменения в файл MyCom.reg в соответствии
со схемой, приведенной ниже, но используя при этом ваши идеитификаторы,
файловые адреса и помня о правилах переноса. Сохраните исправления и зарегистрируйте
все перечисленные объекты, дважды щелкнув на файле MyCom.reg в окне Windows
File Manager:
REGEDIT HKEY_CLASSES_ROOT\MyComTLib.CoSay\CLSID
=
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}
HKEY_CLASSES_ROOT\CLSID\
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}
= MyComTLib.CoSay
HKEY_CLASSES_ROOT\CLSID\
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}
\InprocServer32 =
D:\My Projects\MyComTLib\Debug\MyComTLib.dll'
HKEY_CLASSES_ROOT\CLSID\
{9B865820-2FFA-lld5-98B4-OOE0293F01B2}\TypeLib =
{0934DA90-608D-4107-9ECC-C7E828AD0928}
HKEY_CLASSES_ROOT\TypeLib\
{0934DA90-608D-4107-9ECC-C7E828AD0928}
= MyComTLib
HKEY_CLASSES_ROOT\TypeLib\
{0934DA90-608D-4107-9ECC-C7E828AD0928}
\1.0\0\Win32 =
D:\My Projects\MyComTLib\Debug\MyComTLib.tlb
После этого
дайте команду Build > Rebuild Solution. При осуществлении компоновки
(Linking) в окне Output должна появиться строка:
Creating library Debug/MyComTLib.lib
and object Debug/MyComTLib.exp
которая свидетельствует
о том, что DEF-файл воспринят и участвует в построении проекта. Если вы не видите
этой строки, то выполните шаги по настройке проекта, которые описаны выше в
разделе «Файл описания DLL», и повторите процедуру построения. После этого сервер
готов к использованию.
Разработка клиента с
использованием специальных указателей
Создайте новый
пустой проект консольного приложения с именем SayTLibClient и вставьте в него
новый файл SayTLibClient.cpp. Введите в файл следующий текст и проследите за
тем, чтобы текст директивы #import либо не разрывался переносом ее продолжения
на другую строку, либо разрывался по правилам, то есть с использованием символа
переноса ' \ ', как вы видите в тексте книги. После этого запустите проект на
выполнение (Ctrl+F5):
#import "C:\MyProjects\MyComTLib\Debug\ MyComTLib.tlb" \
no_namespace
named_guids
void main()
{
Colnitialize(0);
//====== Используем
"умный" указатель
ISayPtr pSay(CLSID_CoSay);
pSay->Say();
pSay->SetWord(L"The
client now uses smart pointers!");
pSay->Say();
pSay=0;
CoUninitialize();
}
Несмотря на
то что здесь нет многих строчек кода, присутствовавшего в предыдущей версии
клиентского приложения, новая версия тоже должна работать. Попробуем разобраться
в том, как это происходит.
Директивой
tfimport можно пользоваться для генерации кода не только на основе TLB-файлов,
но также и на основе других двоичных файлов, например ЕХЕ-, DLL- или OCX-файлов.
Важно, чтобы в этих файлах была информация о типах СОМ-объекте в.
Вы можете
увидеть результат воздействия директивы #import на плоды работы компилятора
C++ в папке Debug. Там появились два новых файла заголовков: MyCoTLib.tlh (type
library header) и MyComTLib.tli (type library implementations). Первый файл
подключает код второго (именно в таком порядке) и они оба компилируются так,
как если бы были подключены директивой #include. Этот процесс конвертации двоичной
библиотеки типов в исходный код C++ дает возможность решить довольно сложную
задачу обнаружения ошибок при пользовании данными о СОМ-объекте. Ошибки, присутствующие
в двоичном коде, трудно диагностировать, а ошибки в исходном коде выявляет и
указывает компилятор. В данный момент важно не потерять из виду цепь преобразований:
Немного позже
мы рассмотрим содержимое новых файлов, а сейчас обратите внимание на то, что
директива # import сопровождается двумя атрибутами: no_namespace и named_guids,
которые помогают компилятору создавать файлы заголовков. Иногда содержимое библиотеки
типов определяется в отдельном пространстве имен (namespace), чтобы избежать
случайного совпадения имен. Пространство имен определяется в контексте оператора
library, который вы видели в IDL-фай-ле. Но в нашем случае пространство имен
не было указано, и поэтому в директиве #import задан атрибут no_namespace. Второй
атрибут (named_guids) указывает компилятору, что надо определить и инициализировать
переменные типа GUID в определенном (старом) стиле: ывю_муСот, CLSiD_CoSay и
iio_isay. Новый стиль задания идентификаторов заключается в использовании операции
_uuidof(expression). Microsoft-расширение языка C++ определяет ключевое слово
_uuidof и связанную с ним операцию. Она позволяет добыть GUID объекта, стоящего
в скобках. Для ее успешной работы необходимо прикрепить GUID к структуре или
классу. Это действие выполняют строки вида:
struct declspec(uuid("9b865820-2ffa-1Id5-98b4-00e0293f01b2"))
/* LIBID */ _MyCom;
которые также
используют Microsoft-расширение языка C++ (declspec). Рассматриваемые новшества
вы в изобилии увидите, если откроете файл MyCoTLib.tlh:
// Created by
Microsoft (R) C/C++ Compiler.
//
// d:\my projects\saytlibclient\debug\MyComTLib.tlh
//
// C++ source
equivalent of Win32 type library
// D:\My Projects\MyComTLib\Debug\MyComTLib.tlb
// compiler-generated
file. - DO NOT EDIT!
#pragma once
#pragma pack(push,
8)
#include<comdef.h>
//
// Forward references
and typedefs //
struct __declspec(uuid("0934da90-608d-4107
-9eccc7e828ad0928"))
/* LIBID */ _MyCom; struct /* coclass */ CoSay;
struct _declspec(uuid("170368dO-85be
-43af-ae71053f506657a2"))
/* interface */ ISay;
{
//
// Smart pointer
typedef declarations //
_COM_SMARTPTR_TYPEDEF(ISay,
_uuidof(ISay));
//
// Type library
items
//
struct _declspec(uuid("9b865820-2ffa
-lld5-98b4-00e0293f01b2"))
CoSay;
// [ default
] interface ISay
struct _declspec(uuid("170368dO-85be
-43af-ae71-053f506657a2")) ISay : lUnknown
{
//
// Wrapper methods
for error-handling
//
HRESULT Say (
) ;
HRESULT SetWord
(_bstr_t word ) ;
//
// Raw methods
provided by interface -
//
virtual HRESULT
_stdcall raw_Say ( ) = 0;
virtual HRESULT _stdcall raw_SetWord
( /*[in]*/ BSTR word ) = 0;
};
//
// Named GUID
constants initializations
//
extern "C" const GUID _declspec(selectany)
LIBID_MyCom =
{Ox0934da90, Ox608d, 0x4107,
{.Ox9e, Oxcc, Oxc7, Oxe8, 0x28, Oxad, 0x09, 0x28} } ;
extern "C"
const GUID __declspec(selectany) CLSID_CoSay =
{Ox9b865820,0x2ffa,OxlId5,
{0x98,Oxb4,0x00,OxeO,0x29,Ox3f,0x01,Oxb2}};
extern "C" const GUID __declspec(selectany) IID_ISay =
{
0xl70368dO,Ox85be,0x43af,
{0xae,0x71,0x05,Ox3f,0x50,Охбб, 0x57,Oxa2}
};
//
// Wrapper method
implementations //
#include "c:\myprojects\saytlibclient
\debug\MyComTLib.tli"
#pragma pack(pop)
Код TLH-файла
имеет шаблонную структуру. Для нас наибольший интерес представляет код, который
следует после упреждающих объявлений регистрируемых объектов. Это объявление
специального (smart) указателя:
_COM_SMARTPTR_TYPEDEF(ISay,
_uuidof(ISay));
Для того чтобы
добавить секретности, здесь опять использован макрос, который при расширении
превратится в:
typedef
_com_ptr_t<_com_IIID<ISay,
_uuidof(ISay)> > ISayPtr;
Как вы, вероятно,
догадались, лексемы _com_lliD и com_ptr_t представляют собой шаблоны классов,
первый из них создает новый класс C++, который инкапсулирует функциональность
зарегистрированного интерфейса ISay, а второй — класс указателя на этот класс.
Операция typedef удостоверяет появление нового типа данных ISayPtr. Отныне объекты
типа ISayPtr являются указателями на класс, скроенный по сложному шаблону. Цель
— избавить пользователя от необходимости следить за счетчиком ссылок на интерфейс
isay, то есть вызывать методы AddRef и Release, и устранить необходимость вызова
функции CoCreatelnstance. Заботы о выполнении всех этих операций берет на себя
новый класс. Он таким образом скрывает от пользователя рутинную часть работы
с объектом СОМ, оставляя лишь творческую. В этом и заключается смысл качественной
характеристики smart pointer («сообразительный» указатель).
Характерно
также то, что методы нашего интерфейса (Say и SetWord) заменяются на эквивалентные
виртуальные методы нового шаблонного класса (raw_say и raw_setword). Сейчас
уместно вновь проанализировать код клиентского приложения и постараться увидеть
его в новом свете, зная о существовании нового типа ISayPtr. Теперь становится
понятной строка объявления:
ISayPtr pSay
(CLSID_CoSay);
которая создает
объект pSay класса, эквивалентного типу ISayPtr. При этом вызывается конструктор
класса. Начиная с этого момента вы можете использовать smart указатель pSay
для вызова методов интерфейса ISay. Рассмотрим содержимое второго файла заголовков
MyComTLib.tli:
// Created by
Microsoft (R) C/C++ Compiler.
//
// d:\my projects\saytlibclient\debug\MyComTLib.tli
//
// Wrapper implementations
for Win32 type library
// D:\My Projects\MyComTLib\Debug\MyComTLib.tlb
// compiler-generated
file. - DO NOT EDIT!
#pragma once
//
// interface
ISay wrapper method implementations
//
inline HRESULT
ISay::Say ( )
HRESULT _hr = raw_Say();
if (FAILED(_hr))
_com_issue_errorex(_hr, this,_uuidof(this));
return _hr;
inline HRESULT ISay : :SetWord ( _bstr_t word )
{
HRESULT _hr -
raw_SetWord(word) ;
if (FAILED
(_hr) )
_com_issue_errorex
(_hr, this, _ uuidof (this) );
return _hr;
}
Как вы видите,
здесь расположены тела wrapper-методов, заменяющих методы нашего интерфейса.
Вместо прямых вызовов методов Say и Setword теперь будут происходить косвенные
их вызовы из функций-оберток (raw_Say и raw_SetWord), но при этом исчезает необходимость
вызывать методы Createlnstance и Release. Подведем итог. СОМ-интерфейс первоначально
представлен в виде базового абстрактного класса, методы которого раскрываются
с помощью ко-класса. При использовании библиотеки типов некоторые из его чисто
виртуальных функций заменяются на не виртуальные inline-функции класса-обертки,
которые внутри содержат вызовы виртуальных функций и затем проверяют код ошибки.
В случае сбоя вызывается обработчик ошибок _com_issue_errorex. Таким образом
smart-указатели помогают обрабатывать ошибки и упрощают поддержку счетчиков
ссылок.
В
рассматриваемом коде использован специальный miacc_bstr_t предназначенный для
работы с Unicode-строками. Он является классом-оберткой для BSTR, упрощающим
работу со строками типа B.STR. Теперь можно не заботиться о вызове функции SysFreeString,
так как эту работу берет на себя класс _bstr_t.
Библиотеки
шаблонов, такие как ATL (Active Template Library), отличаются от обычных библиотек
классов C++ тем, что они представляют собой множество шаблонов (templates),
которые могут и не иметь иерархической структуры. При использовании обычной
библиотеки мы создаем класс, производный от какого-то класса из библиотеки и
тем самым наследуем всю его функциональность, а значит, и функциональность его
предков. С библиотекой шаблонов поступают по-другому. Выбрав шаблон, обращаются
к нему для создания нового, класса, .скроенного по образу и подобию шаблона,
получая тем самым его общую функциональность. Специфика определяется путем реализации
некоторых методов шаблона. Новый класс кроится по шаблону, настраиваемому параметром,
который передается в угловых скобках шаблона.
Использование
библиотеки ATL полностью снимает с вас заботу о реализации методов ILJnknown,
о получении уникальных идентификаторов и регистрации их в системе, а также многие
другие рутинные проблемы, связанные с поддержкой технологии СОМ. Вы теперь сможете
оценить эти преимущества, так как попробовали создать СОМ-объект с помощью сырых
(raw) COM API. У нас нет времени более подробно заниматься технологией СОМ,
так как общая направленность книги — использование передовых технологий, а не
детальное их изучение. Для получения фундаментальных знаний о технологии мы
отсылаем читателя к книгам, перечисленным ранее. Отметим, что текст книги Inside
OLE целиком (1200 страниц) помещен в MSDN (см. раздел Books).
Далее рассмотрим, как создать СОМ-объект, обладающий возможностями DLL-сервера (inproc server), Мы создадим новый проект, а в нем остов СОМ DLL-сервера и добавим необходимый нам код, учитывающий специфику СОМ-объекта.
Итак, СОМ
DLL-сервер или дом для ко-классов готов. Теперь можно начать процесс
начинки его классами (или одним классом), которые, в свою очередь, будут являться
домами для экспонируемых интерфейсов. Говорят, что ко-класс реализовывает или
экспонирует интерфейсы (или один интерфейс). Просмотрите результаты работы мастера.
В файле ATLGL.cpp, здесь уже нарушена традиция MFC разделять объявление и реализацию
класса, объявлен класс CATLGLModule, скроенный по шаблону и одновременно производный
от класса CAtlDllModuleT. К сожалению, документация по ATL содержит весьма краткие
сведения о своих классах. Из нее мы можем, однако, узнать, что шаблон классов
CAtlDllModuleT поддерживает функциональность DLL-модуля, который умеет регистрировать
себя в качестве такового. Он происходит от класса CAtiModule, у которого есть
симметричный потомок CAtlExeModuleT, поддерживающий функциональность ЕХЕ-модуля
приложения, и умеет обрабатывать параметры командной строки. Иначе такой модуль
называется out-of-proc-сервером (локальным или удаленным сервером). Он выполняется
в пространстве собственного процесса, а не клиентского, как в случае in-proc-сервера.
Аналогично MFC-проекту, в котором есть объект theApp, здесь объявлен глобальный объект _AtlModule класса CATLGLModule, унаследованные методы которого позволяют зарегистрировать (DllRegisterServer) в системном реестре наличие нового сервера COM DLL. Но это только начало. Немного позже мы создадим и зарегистрируем СОМ-объект, все его интерфейсы и библиотеку (typelib) упреждающего описания новых объектов COM (coclass, interface, dispinterface, module, typedef). Да, каждый СОМ-объект вносит довольно много записей в системный реестр, поэтому так важно правильно производить обратную процедуру (DllUnregisterServer), иначе реестр превращается в кладбище записей, внесенных объектами, которые уже не существуют в операционной системе.
Вы уже знаете,
что созданный и подключенный компоновщиком динамический модуль система интегрирует
в пространство другого (клиентского) процесса, загрузив его по определенному
базовому адресу. Любая динамически загружаемая библиотека экспортирует функции,
которые пишутся в расчете на то, что их будет вызывать клиентское приложение
или другая DLL. Глобальная функция DllMain представляет собой точку входа в
динамически подключаемую библиотеку. Она является некоторого рода заглушкой
(placeholder) для реального, определяемого библиотекой имени функции. Первый
параметр DllMain подан операционной системой и представляет собой Windows-описатель
DLL. Его можно использовать при вызове функций, требующих этот описатель, например
при вызове GetModuleFileName. Второй параметр указывает причину вызова DLL.
Он может принимать одно из четырех значений:
Если DllMain
вернет FALSE или 0, то клиентское приложение завершится с кодом ошибки. Характерно,
что стратегия работы с СОМ-объектами сходна со стратегией, используемой при
работе с DLL. Последняя заключается в том, что каждый вызов функции LoadLibrary
увеличивает на единицу счетчик числа пользователей библиотеки. Вызов функции
FreeLibrary уменьшает значение счетчика. Обнаружив, что счетчик числа пользователей
равен нулю, операционная система автоматически выгрузит ее. Если после этого
вызвать какую-либо экспортируемую DLL функцию, то возникнет исключительная ситуация
Access Violation, так как код по указанному адресу уже не отображается на адресное
пространство процесса.
Возвращаясь
к коду, созданному мастером ATL Project wizard, отметим, что кроме DllMain,
модуль экспортирует еще 4 функции: DllRegisterServer, DllUnregisterServer, DllCanUnloadNow,
DllGetClassObject. Полезно открыть, с помощью окна Solution Explorer файл ATLGL.def,
который создал и поместил в папку проекта мастер. Этот файл используется компоновщиком
при создании lib-файлов и ехр-файлов, содержащих информацию о DLL и экспортируемых
ею функциях. Все эти функции имеют тип STDAPI. На самом деле STDAPI — это макроподстановка,
заданная в файле заголовков WinNT.h. С помощью этого файла вы можете самостоятельно
расшифровать макрос STDAPI. Он разворачивается (expanded) в такой комплексный
описатель:
extern
"С" HRESULT _stdcall
Описатель
extern «С» означает, что при вызове функция будет использовать имя в стиле языка
С, а не C++, то есть описатель отменяет декорацию имен, производимую компилятором
C++ по умолчанию.
Компилятор
C++ использует специальную декорацию имен, для того чтобы отличать overloaded-функции,
имеющие одинаковые имена, но разные прото-. типы. Например, вызов: int func(int
a, double b); в результате декорации становится: _func@12. Число 12 описывает
количество байт, занимаемых списком аргументов. Такая условность называется
naming convention (соглашение об именах). Есть и другая конвенция — calling
convention (соглашение о связях), которая определяет договоренность о передаче
параметров при вызове Win32 API-функций. Описатель _stdcall относится к этой
группе. Он определяет: порядок передачи аргументов (справа налево): то, что
аргументы передаются по значению (by value), что вызываемая функция должна сама
выбирать аргументы из стека и что трансляция регистра символов, верхнего или
нижнего, не производится.
Функция DllCanUnloadNow
определяет, используется ли данная DLL в данный момент. Если нет, то вызывающий
процесс может безопасно выгрузить ее из памяти. Функция DllGetClassObject с
помощью третьего параметра (LPVOID* ppv) возвращает адрес так называемой фабрики
классов, которая умеет создавать СОМ-объекты, по известному CLSID — уникальному
идентификатору объекта.
Откройте файл
ATLGLJ.c и.убедитесь, что он пуст. Этот файл будет заполнен кодами компилятором
MIDL, о котором мы уже говорили ранее. Запустите приложение (Ctrl+F5). Компилятор
и компоновщик создадут исполняемый модуль типа DLL, но загрузчик не будет знать
в рамках какого процесса (контейнера) следует запустить его на отладку.
В
этот момент Studio.Net запросит имя ехе-файла, то есть модуля или процесса
в пространство которого должна быть загружена созданная компоновщиком
DLL. Вы можете воспользоваться выпадающим списком для выбора строки Browse,
которая даст диалог по выбору файла. Найдите с его помощью стандартный контейнер
для отладки элементов ActiveX (tstcon32.exe), поставляемый вместе со Studio.Net
по адресу:...\MicrosoftVisualStudio.Net\Common7\Tools и нажмите Open, а затем
ОК.
В рамках тестового контейнера можно отлаживать работу элементов ActiveX, OLE-controls и других СОМ-объектов. Но сейчас наш объект еще не готов к этому, так как мы не создали СОМ-класса, инкапсулирующего желаемые интерфейсы. Поэтому закройте тестовый контейнер, вновь откройте в рамках Studio.Net уже существующий IDL-файл (Interface Description Language file) ATLGLidl и просмотрите коды, описывающие интерфейс, СОМ-класс и библиотеку типов. Вы помните, что этот файл обрабатывается компилятором MIDL, который на его основе создает другие файлы. Откройте файл ATLGM.c и убедитесь, что теперь он не пуст. Его содержимое было создано компилятором MIDL. В данный момент файл ATLGM.c содержит только один идентификатор библиотеки, который регистрируется с помощью макроподстановки MIDL_DEFINE_GUID.
Вернемся в файл ATLGLcpp, где кроме функций, перечисленных выше, присутствуют загадочные макросы. Их смысл довольно прозрачен, но разработчика не должны устраивать догадки, ему нужны более точные знания. Сопровождающая документация, особенно бета-версий, не всегда дает нужные объяснения, поэтому приходится искать их самостоятельно в заголовочных файлах, расположенных по адресу: ...\Microsoft Visual Studio.Net\Vc7\indude или ...\Microsoft Visual Studio.Net\ Vc7\atlmfc\include.
Покажем, как
это делается на примере. Нас интересует смысл функциеподобной макроподстановки:
DECLARE_LIBID(LIBID_ATLGLLib)
В результате
поиска в файлах по указанному пути (маска поиска должна быть *.h) находим (в
файле ATLBase.h), что при разворачивании препроцессором этот макрос превратится
в статическую функцию класса CATLGLModule:
static void InitLibldO throw ()
{
CAtlModule::m_libid = LIBID_ATLGLLib;
}
Теперь возникает
желание узнать, что кроется за идентификатором LiBiD_ATLGLLib. Во вновь созданном
коде файла ATLGM.c находим макрос:
MIDL_DEFINE_GUID(IID,
LIBID_ATLGLLib,ОхЕбОбОЗВС,Ox9DE2,
0x4563, OxA7,0xAF,Ox8A,Ox8C,Ox4E,0x80,0x40,0x58);
узнав смысл
которого мы сможем понять, чем является LiBiD_ATLGLLib. В вашем проекте цифры
будут другими, но я привожу здесь те, которые вижу сейчас, для того чтобы быть
более конкретным и не загружать вас абстракциями, которых и так хватает. В этом
случае поиск не нужен, так как объявление макроса расположено двумя строчками
выше. Вот оно:
#define MIDL_DEFINE_GUID(type,name,1,wl,w2,bl,b2,b3,Ь4, \ Ь5,Ьб,b7,b8)
const type name = \ {I,wl,w2, {b1,b2,bЗ,b4,b5,b6,b7,b8}
}
Подставив
значения параметров из предыдущего макроса, получим определение LiBiD_ATLGLLib,
которое увидит компилятор:
const IID LIBID_ATLGLLib =
{
0xE60605BC, 0x9DE2,
0x4563,
{ 0xA7,0xAF,0x8A, 0x8C,Ox4E, 0x80, 0x40, 0x58 }
}
Отсюда ясно, что LIВID_АТLGLLib — это константная структура типа IID. Осталось узнать, как определен тип данных II D.
В хорошо знакомом файле afxwin.h находим определение typedef GUID IID;. Про Globally Unique Identifier (GUID) сказано очень много, в том числе и в документации Studio.Net. Как мы только что выяснили, изучив работу макросов и LiBio_ATLGLLib, тип IID также используется для идентификации библиотек типов. Система применяет два типа GUID: строковый в реестре, и числовой в клиентских приложениях. Второй макрос, который вы видели в классе
CATLGLModule:
DECLARE_REGISTRY_APPID_RESOURCEID(IDR_ATLGL,
"{E4541023-7425-4AA7-998C-D016DF796716}")
(цифры мои,
ваши будут другими) создает строковый GUID. При расширении он превратится в
три статические функции класса, две из которых готовят текстовую строку того
или иного типа, а третья регистрирует, в случае если bRegister==TRUE, или убирает
из реестра эту строку по адресу HKEY_CLASSES_ROOT\APPID\:
static LPCOLESTR GetAppId ()throw ()
{
//====== Преобразование
к формату OLE-строки
return OLESTR("{E4541023-7425-4AA7-998C-D016DF796716}")
;
}
static TCHAR*
GetAppIdTO throw ()
{
//====== Преобразование
к Unicode или char* строке
return _T("{E4541023-7425-4AA7-998C-D016DF796716}")
;
}
// Если bRegister==TRUE,
то происходит запись в реестр,
// иначе - удаление
записи
static HRESULT
WINAPI UpdateRegistryAppId(BOOL bRegister) throw()
{
_ATL_REGMAP_ENTRY
aMapEntries [] =
{
{ OLESTRC'APPID")
, GetAppIdO }, { NULL, NULL }
};
return ATL::_pAtlModule->UpdateRegistryFromResource(
IDR ATLGL, bRegister, aMapEntries);
В данный момент
вы сможете найти в реестре свой ключ и ассоциированную с ним строку (ATLGL)
по адресу:
HKEY_CLASSES_ROOT\AppID\
{E4541023-7425-4AA7-998C-D016DF796716}
При запуске
приложения вышеописанные функции были вызваны каркасом приложения и произвели
записи в реестр. Отметьте также, что в реестре появилась еще одна (симметричная)
запись по адресу HKEY_CLASSES_ROOT \APPID\ATLGL.DLL. Она ассоциирует строковый
GUID с библиотекой ATLGL.DLL. Рассматриваемая строка-идентификатор встречается
еще в нескольких разделах проекта, найдите их, чтобы получить ориентировку:
в ресурсе "REGISTRY" > IDR_ATLGL (см. окно Resource View) и в файле сценария
регистрации ATL.GL.rgs (см. окно Solution Explorer).
Возвращаясь
к первому макросу DECLARE_LIBID(LiBiojvTLGLLib), отметим, что скрытая за ним
функция initLibid тоже была вызвана каркасом и использована для регистрации
библиотеки типов будущего СОМ-объекта. Вы можете найти эту, значительно более
подробную, запись по ключу (цифры мои):
HKEY_CLASSES_ROOT\TypeLib\
{E60605BC-9DE2-4563-A7AF-8A8C4E804058}
Создание элемента типа ATL Control
Создаваемый
модуль DLL будет содержать в себе элемент управления, который внедряется в окно
клиентского приложения, поэтому в проект следует добавить заготовку нового СОМ-класса,
обладающего функциональностью элемента типа ATL Control. В следующем уроке мы
внесем в него функциональность окна OpenGL, поэтому мы назовем класс OpenGL,
хотя в этом уроке элемент не будет иметь дело с библиотекой Silicon Graphics.
Он будет элементом ActiveX, созданным на основе заготовки ATL. Создать вручную
элемент ActiveX достаточно сложно, поэтому воспользуемся услугами еще одного
мастера Studio.Net. При включении нового мастера (wizard) важно, где установлен
фокус. Отметьте, что сейчас в рабочем пространстве существуют два проекта: один
(ATLGL) — это DLL-сервер, а другой (ATLGLPS) — это коды заглушек proxy/stub.
Просмотрите
результаты работы мастера. Самым крупным его произведением является файл OpenGLh,
который содержит объявление и одновременно коды класса COpenGL. Для ATL-проектов
характерно то, что создаваемые ко-классы наследуют данные и методы от многих
родителей, в число которых входят как СОМ-классы, так и интерфейсы. Другой характерной
чертой является сосредоточение значительной части функциональности в h-файле.
Напрашивается вывод, что некоторые принципы и идеи, отстаиваемые Microsoft в
MFC, были инвертированы в ATL. Сколько полемического задора было растрачено
в критике множественного наследования (намек на Borland OWL) на страницах документации
по MFC, и вот теперь мы видим вновь созданный класс (COpenGL), который имеет
18 родителей, среди которых 5 классов и 13 интерфейсов.
Здесь у вас
опять должна закружиться голова, но не сдавайтесь. Важно не выпускать главную
нить логики приложения. Резон таков: мастера настрочили уйму кода, который пока
непонятен, возможно, и всегда будет таким, но этот код уже работает и нам нужно
где-то встроиться в него, чтобы почувствовать возможность управлять общей логикой
внедряемого элемента ActiveX. Имея под рукой Wizards Studio.Net, это можно сделать,
даже оставаясь в некотором неведении относительно деталей работы интерфейсов
СОМ. Вам не придется вручную реализовывать ни одного интерфейса. Вы можете сосредоточиться
только на алгоритме работы самого элемента, то есть на том, что вы должны продемонстрировать
пользователю вашего объекта.
Запустите
приложение, но на этот раз не закрывайте тестовый контейнер, который должен
запуститься автоматически, без вашего участия. В окне тестового контейнера вы
не увидите признаков нашего элемента, так как он еще не загружен. Дайте команду
Edit > IhsertNew Control. После некоторой паузы, в течение которой
контейнер собирает информацию из реестра обо всех элементах OLE Controls, вы
увидите диалоговое окно с длинным списком элементов, о которых есть информация
в реестре.
Это
совсем не означает, что все элементы живы и здоровы. На мой взгляд, ситуация
уже вырастает в серьезную проблему. В систему следует ввести эффективные средства
корректировки реестра, потому что совсем неинтересно проводить часы драгоценного
времени, копаясь в реестре или инструменте типа OLE/COM Object Viewer (Просмотр
объектов OLE/COM) и выясняя, жив элемент или его давно нет. Может быть, как
говорят политики, я не владею информацией, но все программки типа CleanRegistry
либо опасны, либо мало полезны и неэффективны.
При открытом
окне диалога Insert Control вы можете просто ввести букву о — начальную букву
нашего элемента OpenGL. Теперь, используя клавиши навигации по списку (стрелки),
быстро найдете в нем строку OpenGL Class. Выберите ее и нажмите ОК. Вы должны
увидеть окно внедренного элемента, которое выглядит так, как показано на рис.
8.2.
Рис.
8.2. Стартовая заготовка элемента ActiveX в окне тестового контейнера
Загляните
в файл ATLGLJ.c и увидите три новых макроса типа MIDL_DEFINE_GUID, которые уже
выполнили свою работу и поместили в реестр множество новых записей по адресам:
HKEY_CLASSES_ROOT\ATLGL.OpenGL\
HKEY_CLASSES_ROOT\ATLGL.OpenGL.1\
HKEY_CLASSES_ROOT\CLSID\
HKEY_CLASSES_ROOT\
Interface\
Когда клиент
СОМ-объекта пользуется услугами локального или удаленного сервера, то есть когда
данные передаются через границы различных процессов или между узлами сети, требуется
поддержка маршалинга (marshaling). Так называется процесс упаковки и посылки
параметров, передаваемых методам интерфейсов через границы потоков или процессов,
который мы слегка затронули ранее. Вы помните, что MIDL генерирует код на языке
С для двух компонентов: Proxy (представитель СОМ-объекта на стороне клиента)
и stub (заглушка на стороне СОМ-сервера). Эти компоненты общаются между собой
и решают проблемы Вавилонской башни, то есть преодолевают сложности обмена данными,
возникающими из-за
того, что клиент и сервер используют различные типы данных — разговаривают на
разных языках. Чтобы увидеть проблему, надо ее создать. Интересно то, что при
объяснении необходимости этого чудовищного сооружения:
приводится
соображение о том, что программы на разных языках программирования смогут общаться,
то есть обмениваться данными. Как мы уже обсуждали, разработчики имеют в виду
четыре языка, два из которых реально используются (Visual C++ и Visual Basic),
а два других (VBScript и Visual J++) едва подают признаки жизни. Правда здесь
надо учесть бурное развитие нового языка с#, который, очевидно, тоже участвует
в движении СОМ.
Откройте файл
ATLGLidl и постарайтесь вникнуть в смысл новых записей, не отвлекаясь на изучение
языка IDL, который потребует от вас заметных усилий и временных затрат. Прежде
всего отметьте, что в библиотеке типов (library ATLGLLib), сопровождающей наш
СОМ-объект, появилось описание СОМ-класса
coclass OpenGL
{
[default]
interface IQpenGL;
[default, source] dispinterface _IOpenGLEvents;
};
который предоставляет
своим пользователям два интерфейса. Я не привожу здесь предшествующий классу
OpenGL блок описаний в квадратных скобках, который носит вспомогательный характер.
Элементы ActiveX используют события (events) для того, чтобы уведомить приложение-контейнер
об изменениях в состоянии объекта в результате действий пользователя — манипуляции
посредством мыши и клавиатуры в окне объекта. Найдите описание одного из объявленных
интерфейсов:
dispinterface _IOpenGLEvents
{
properties:
methods:
};
Пока пустые секции properties (свойства): и methods (методы): намекают на то, что мы должны приложить усилия и ввести, с помощью инструментов Studio.Net в разрабатываемый СОМ-объект способность изменять свои свойства и экспортировать методы. Информация о втором интерфейсе расположена вне блока, описывающего библиотеку типов:
interface IQpenGL : IDispatch
{
[propput,
bindable, requestedit, id(DISPID_FILLCOLOR)]
HRESULT FillColor([in]OLE_COLOR
clr);
[propget,
bindable, requestedit, id(DISPID_FILLCOLOR)]
HRESULT FillColor([out, retval]OLE_COLOR* pclr);
};
Технология
Automation, ранее известная как OLE Automation, дает совершенно другой способ
вызова клиентом методов, экспонируемых сервером, чем тот стандартный для СОМ
способ, который мы уже рассмотрели. Вы помните, что он использует таблицу виртуальных
указателей vtable на интерфейсы. Automation же использует стандартный СОМ-интерфейс
IDispatch для доступа к интерфейсам. Поэтому говорят, что любой объект, поддерживающий
IDispatch, реализует Automation. Также говорят о дуальном интерфейсе, имея в
виду, что он может быть вызван как с помощью естественного способа (vtable),
так и с помощью вычурного способа Automation. Итак, интерфейс IOpenGL предоставляет
своим пользователям двойственный (dual) интерфейс.
Dual Interface
понадобился для того, чтобы VBScript-сценарий мог использовать СОМ-объекты,
созданные с помощью Visual C++. Клиенты, созданные на языке C++, могут с помощью
Querylnterf асе получить адрес интерфейса и прямо вызывать его методы, пользуясь
таблицей виртуальных функций (vtable), например:
p->SomeMethod(i,
d);
В VBScript
будут проблемы. Там нет строгого контроля соответствия типов и многие типы C++
ему неизвестны. Интерфейс IDispatch служит посредником в разговоре двух произведений
Microsoft. Теперь программа на VBScript может добраться до метода SomeMethod,
выполнив длинную цепь вызовов. Сначала она должна получить указатель на интерфейс
IDispatch, затем с его помощью (GetiDsOf Names) узнать индекс желаемого метода
(типа DISPID — dispatch identifier), на сей раз не 128-битный. После этого она
сможет заставить систему выполнить коды метода SomeMethod, но не прямо, а с
помощью метода IDispatch: : Invoke, который требует задать 8 параметров, смысл
которых может приблизительно соответствовать следующему списку описаний. Последующий
текст воспринимайте очень серьезно, так как он взят прямо из справки IDispatch::
invoke:
(Поток сознания в скобках, по Джойсу или Жванецкому: новые концепции, новые технологии, глубина мыслей, отточенность деталей, настоящая теория должна быть красивой, тупиковая ветвь?, монополисты не только заставляют покупать, но и навязывают свой способ мышления, что бы ты делал без MS, о чем думал, посмотри CLSID в реестре, видел ли я полезный элемент ActiveX, нужно ли бесшовно внедрять что-нибудь во что-нибудь, посмотри Interfaces в реестре, что лучше, Stingray-класс или внедренная по стандарту OLE таблица Excel, тонкий (thin) клиент не будет иметь кода, но будет иметь много картинок и часто покупать дешевые сеансы обслуживания, как раньше билеты в кино или баню, если не поддерживать обратную совместимость, то кто будет покупать, лучше не купить, чем перестать играть в DOS-игры, стройный (slim) клиент, хочешь, еще посчитаю — плати доллар, перестань думать, пора работать.)
Дуальные или интерфейсы диспетчеризации (dispinterfaces) в отличие от тех vtable-интерфейсов, с которыми вы уже знакомы, были разработаны для того, чтобы реализовать позднее связывание (late-binding) клиента с сервером. Инструментальная среда разработки Visual Basic в этом смысле является лидером, так как в ней вы почти без усилий можете создать приложение, способное на этапе выполнения, то есть поздно, получить информацию от объекта и пользоваться методами интерфейсов, информация о которых стала доступной благодаря IDispatch.
Стандартные свойства
Возвращаясь
к нашему проекту, отметим, что интерфейс юрепсъ предоставляет своим пользователям
два одноименных метода FillColor. Первый метод позволяет пользователю изменить
(propput) стандартное или встроенное (stock property) свойство: «цвет заливки».
Второй — узнать (propget) текущее значение этого свойства. Этот интерфейс был
вставлен мастером потому, что при создании элемента мы указали на -необходимость
введения в него одного из стандартных свойств. С этой же целью мастер ввел в
состав класса переменную:
OLE_COLOR m_clrFillColor;
которая будет
хранить значение свойства. Мы должны ею управлять, поэтому давайте зададим начальное
значение цвета в конструкторе класса. Найдите его и измените:
COpenGL()
{
m_clrFillColor = RGB (255,230,255);
}
Но этого мало.
Для того чтобы увидеть результат, надо изменить коды функции рисования, которую
вы найдете в том же файле OpenGLh.
Вступив
в царство ATL, придется отречься от многих привычек, приобретенных в MFC. Вы
уже заметили, что мы теперь вместо char* или CString пользуемся OLESTR, а вместо
COLORREF— OLE_COLOR. Это еще не так отвлекает, но вот теперь надо рисовать без
помощи привычного класса CDC и вернуться к описателю НОС контекста устройства,
которым мы пользовались при разработке традиционного Windows-приложения на основе
функций API. Также придется привыкнуть к тому, что описатель HOC hdcDraw упрятан
в структуру типа ATL_DRAWINFO, ссылку на которую мы получаем в параметре метода
OnDraw класса CComControl.
Напомню, что
вся функциональность класса CComControl унаследована нашим классом COpenGL,
который, кроме него, имеет еще 17 родителей. Состав полей структуры ATL_DRAWINFO
не будем приводить здесь, чтобы не усугублять головокружение, а вместо этого
предложим убедиться в том, что можно влиять на облик СОМ-объекта. Особенностью
перерисовки СОМ-объекта является то, что он изображает себя в чужом окне. Поэтому,
получив контекст устройства, связанный с этим окном, он должен постараться не
рисовать вне пределов прямоугольника, отведенного для него. В Windows существует
понятие поврежденной области окна (clip region). Это обычно прямоугольная область,
в пределах которой система позволяет приложению рисовать. Если рисующие функции
GDI попробуют выйти за границы этой области, то система не отобразит этих изменений.
Следующий код интенсивно работает с clip region, поэтому для понимания алгоритма
рекомендуем получить справку о функциях GetClipRgn и SelectClipRgn. Введите
изменения в уже существующее тело функции OnDraw так, чтобы она приобрела вид:
HRESULT OnDraw(ATL_DRAWINFO& di)
{
//===== Преобразование
RECTL в RECT
RECT& r =
*(RECT*)di.prcBounds;
//===== Запоминаем
текущую поврежденную область
HRGN hRgnOld
= 0;
//== Функция GetClipRgn может возвратить: 0, 1 или -1
if (GetClipRgn(di.hdcDraw,
hRgnOld) != 1) hRgnOld = 0;
//====== Создание
новой области
HRGN hRgnNew
= CreateRectRgn(r.left,r.top, r.right,r.bottom);
// Оптимистический прогноз (новая область воспринята)
bool bSelectOldRgn
= false;
//=== Устанавливаем
поврежденную область равной г
if (hRgnNew)
{
bSelectOldRgn = SelectClipRgn(di.hdcDraw,hRgnNew) == ERROR;
}
//=== Изменяем
цвет фона и обрамляем объект
::rSelectObject(di.hdcDraw,
::CreateSolidBrush(m_clrFillColor));
Rectangle(di.hdcDraw, r.left, r.top,r.right,r.bottom);
//=== Параметры выравнивания текста и сам текст
SetTextAlign(di.hdcDraw, TA_CENTER | TA_BASELINE);
LPCTSTR pszText = _T("ATL 4.0 : OpenGL");
//=== Вывод текста в центр прямоугольника
TextOut(di.hdcDraw,
(r.left + r.right)/2,
(r.top + r.bottom)/2,
pszText,Istrlen(pszText));
//=== Если был сбой, то устанавливаем старую область
if (bSelectOldRgn)
SelectClipRgn(di.hdcDraw, hRgnOld);
return S_OK;
}
В этой реализации функции OnDraw мы намеренно пошли на поводу у схемы, предложенной в заготовке. Структура RECTL, на которую указывает prcBounds, идентична структуре RECT, но при заливке она ведет себя на один пиксел лучше (см. справку). Здесь это никак не используется. Автору фрагмента не хотелось много раз писать выражение di. prcBounds->, поэтому он завел ссылку на объект типа RECTL, приведя ее к типу RECT. Здесь хочется «взять в руки» CRect, cstring и переписать фрагмент заново в более компактной форме, однако если вы попробуете это сделать, то получите сообщения о том, что CRect и cstring — неизвестные сущности. Они из другого царства MFC. Мы можем подключить поддержку MFC, но при этом многое потеряем. Одной из причин создания ATL была неповоротливость объектов на основе MFC в условиях web-страниц. Мы не можем себе этого позволить, так как собираемся работать с трехмерной графикой. Поэтому надо привыкать работать по правилам Win32-API и классов СОМ.
Тестирование объекта
Вновь запустите
приложение и убедитесь в том, что нам удалось слегка подкрасить объект. Теперь
исследуем функциональность, которую получили бесплатно при оформлении заказа
у мастера.
Попробуем
это исправить. Событие, заключающееся в том, что пользователь объекта изменил
одно из его стандартных свойств, поддерживаемых страницами не менее стандартного
диалога, будет обработано каркасом СОМ-сервера и при этом вызвана функция copenGL:
:OnFillColorChanged, код которой мы не трогали. Сейчас там есть только одна
строка:
ATLTRACE(_T ("OnFillColorChanged\n"));
которая в
режиме отладки (F5) выводит в окно Debug Studio.Net текстовое сообщение. Внесите
в тело этой функции изменения:
void OnFillColorChangedO
{
//======
Если выбран системный цвет,
if (m_clrFillColor
& 0x80000000)
//====== то выбираем
его по индексу
m_clrFillColor=::GetSysColor(m_clrFillColor & Oxlf); ATLTRACE(_T("OnFillColorChanged\n"));
}
Признаком
выбора системного цвета является единица в старшем разряде m_clrFillColor. В
этом случае цвет задан не тремя байтами (red, green, blue), a индексом в таблице
системных цветов (см. справку по GetSysColor). Выделяя этот случай, мы выбираем
системный цвет с помощью API-функции GetSysColor. Заодно подправим функцию перерисовки,
чтобы убедиться, что объект нам подчиняется и мы умеем убирать лишний код:
HRESULT OnDraw(ATL_DRAWINFO& di)
{
//====== Не будем
преобразовывать в RECT
LPCRECTL р =
di.prcBounds;
//====== Цвет
подложки текста
::SetBkColor(di.hdcDraw,m_clrFillColor)
;
//====== Инвертируем
цвет текста
::SetTextColor(di.hdcDraw, ~m_clrFillColor & Oxffffff);
//====== Цвет
фона
::SelectObject(di.hdcDraw,
::CreateSolidBrush(m_clrFillColor));
Rectangle(di.hdcDraw,
p->left, p->top, p->right, p->bottom);
SetTextAlign(di.hdcDraw, TA_CENTER | TA_BASELINE);
LPCTSTR pszText = _T("ATL 4.0 : OpenGL");
TextOut(di.hdcDraw, (p->left + p->right)/2,
(p->top + p->bottom)/2,
pszText, Istrlen(pszText)
};
return S_OK;
}
Запустите и убедитесь, что системные цвета выбираются корректно, а перерисовка при изменении размеров объекта не нарушает заданных границ. Некоторые проблемы возникают при инвертировании цвета фона, если он близок к нейтральному (128, 128, 128). В качестве упражнения решите эту проблему самостоятельно.